feat: add inula-adapter/pinia 和 inula-adapter/vuex
This commit is contained in:
parent
0f75e48f79
commit
96c4f0c337
|
@ -0,0 +1,7 @@
|
|||
## 适配Vue
|
||||
|
||||
### 生命周期
|
||||
|
||||
### pinia
|
||||
|
||||
### vuex
|
|
@ -0,0 +1,101 @@
|
|||
## pinia接口差异
|
||||
|
||||
1、不支createPinia的相关函数,使用了是没有效果的
|
||||
```js
|
||||
it('not support createPinia', () => {
|
||||
const pinia = createPinia(); // 接口可以调用,但是没有效果
|
||||
const store = useStore(pinia); // 传入pinia也没有效果
|
||||
|
||||
expect(store.name).toBe('a');
|
||||
expect(store.$state.name).toBe('a');
|
||||
expect(pinia.state.value.main.name).toBe('a'); // 不能通过pinia.state.value.main获取store
|
||||
});
|
||||
```
|
||||
|
||||
2、因为不支持createPinia,同样也不支持setActivePinia、getActivePinia()
|
||||
|
||||
3、不支持store.$patch
|
||||
```js
|
||||
it('can not be set with patch', () => {
|
||||
const pinia = createPinia();
|
||||
const store = useStore(pinia);
|
||||
|
||||
store.$patch({ name: 'a' }); // 不支持
|
||||
// 改成
|
||||
store.$state.name = 'a'; // 可以改成直接赋值
|
||||
|
||||
expect(store.name).toBe('a');
|
||||
});
|
||||
```
|
||||
|
||||
4、不支持store.$reset();
|
||||
```ts
|
||||
it('can not reset the state', () => {
|
||||
const store = useStore();
|
||||
store.name = 'Ed';
|
||||
store.nested.n++;
|
||||
store.$reset(); // 不支持
|
||||
expect(store.$state).toEqual({
|
||||
counter: 0,
|
||||
name: 'Eduardo',
|
||||
nested: {
|
||||
n: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
5、不支持store.$dispose,可以用store.$unsubscribe代替
|
||||
```js
|
||||
it('can not be disposed', () => {
|
||||
const useStore = defineStore({
|
||||
id: 'main',
|
||||
state: () => ({ n: 0 }),
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
const spy = vi.fn();
|
||||
|
||||
store.$subscribe(spy, { flush: 'sync' });
|
||||
store.$state.n++;
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(useStore()).toBe(store);
|
||||
|
||||
// store.$dispose();
|
||||
// 改成
|
||||
store.$unsubscribe(spy);
|
||||
|
||||
store.$state.n++;
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
```
|
||||
|
||||
6、支持$subscribe,不需要flush,默认就是sync
|
||||
```js
|
||||
it('can be $unsubscribe', () => {
|
||||
const useStore = defineStore({
|
||||
id: 'main',
|
||||
state: () => ({ n: 0 }),
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
const spy = vi.fn();
|
||||
|
||||
// store.$subscribe(spy, { flush: 'sync' });
|
||||
// 不需要flush,默认就是sync
|
||||
store.$subscribe(spy, { flush: 'sync' });
|
||||
store.$state.n++;
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(useStore()).toBe(store);
|
||||
store.$unsubscribe(spy);
|
||||
store.$state.n++;
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
## 适配Vue
|
||||
|
||||
### 生命周期
|
||||
onBeforeMount
|
||||
onMounted
|
||||
onBeforeUpdate
|
||||
onUpdated
|
||||
onBeforeUnmount
|
||||
onUnmounted
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
## vuex接口差异
|
||||
|
||||
1、createStore接口中的,state、mutations、actions、getters不能用相同名字属性
|
||||
```js
|
||||
it('dispatching actions, sync', () => {
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 1
|
||||
},
|
||||
mutations: {
|
||||
[TEST] (state, n) {
|
||||
state.a += n
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
[TEST] ({ commit }, n) {
|
||||
commit(TEST, n)
|
||||
}
|
||||
}
|
||||
})
|
||||
store.dispatch(TEST, 2)
|
||||
expect(store.state.a).toBe(3)
|
||||
})
|
||||
```
|
||||
|
||||
2、createStore接口不支持strict属性
|
||||
```js
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 1
|
||||
},
|
||||
mutations: {
|
||||
[TEST] (state, n) {
|
||||
state.a += n
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
[TEST] ({ commit }, n) {
|
||||
commit(TEST, n)
|
||||
}
|
||||
},
|
||||
strict: true
|
||||
})
|
||||
```
|
||||
|
||||
3、store.registerModule不支持传入数组,只支持一层的module注册
|
||||
```js
|
||||
it('dynamic module registration with namespace inheritance', () => {
|
||||
const store = createStore({
|
||||
modules: {
|
||||
a: {
|
||||
namespaced: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const actionSpy = vi.fn();
|
||||
const mutationSpy = vi.fn();
|
||||
store.registerModule(['a', 'b'], {
|
||||
state: { value: 1 },
|
||||
getters: { foo: state => state.value },
|
||||
actions: { foo: actionSpy },
|
||||
mutations: { foo: mutationSpy },
|
||||
});
|
||||
|
||||
expect(store.state.a.b.value).toBe(1);
|
||||
expect(store.getters['a/foo']).toBe(1);
|
||||
|
||||
store.dispatch('a/foo');
|
||||
expect(actionSpy).toHaveBeenCalled();
|
||||
|
||||
store.commit('a/foo');
|
||||
expect(mutationSpy).toHaveBeenCalled();
|
||||
});
|
||||
```
|
||||
4、createStore不支持多层mudule注册
|
||||
```js
|
||||
it('module: mutation', function () {
|
||||
const mutations = {
|
||||
[TEST](state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
};
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 1,
|
||||
},
|
||||
mutations,
|
||||
modules: { // 第一层module,支持
|
||||
nested: {
|
||||
state: {a: 2},
|
||||
mutations,
|
||||
modules: { // 第二层module,不支持
|
||||
one: {
|
||||
state: {a: 3},
|
||||
mutations,
|
||||
},
|
||||
nested: {
|
||||
modules: { // 第三层module,不支持
|
||||
two: {
|
||||
state: {a: 4},
|
||||
mutations,
|
||||
},
|
||||
three: {
|
||||
state: {a: 5},
|
||||
mutations,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
four: {
|
||||
state: {a: 6},
|
||||
mutations,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
4、不支持store.replaceState
|
||||
```js
|
||||
const store = createStore({});
|
||||
store.replaceState({ a: { foo: 'state' } });
|
||||
```
|
||||
5、store.registerModule不支持传入options参数
|
||||
```js
|
||||
store.registerModule(
|
||||
'a',
|
||||
{
|
||||
namespaced: true,
|
||||
getters: { foo: state => state.foo },
|
||||
actions: { foo: actionSpy },
|
||||
mutations: { foo: mutationSpy },
|
||||
},
|
||||
{ preserveState: true }
|
||||
);
|
||||
```
|
||||
|
||||
5、dispatch不支持传入options参数
|
||||
```js
|
||||
dispatch('foo', null, {root: true});
|
||||
```
|
||||
|
||||
6、不支持action中的root属性,action需要是个函数
|
||||
```js
|
||||
const store = createStore({
|
||||
modules: {
|
||||
a: {
|
||||
namespaced: true,
|
||||
actions: {
|
||||
[TEST]: {
|
||||
root: true,
|
||||
handler() {
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
7、createStore中不支持plugins
|
||||
```js
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 1,
|
||||
},
|
||||
mutations: {
|
||||
[TEST](state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
[TEST]: actionSpy,
|
||||
},
|
||||
plugins: [ // 不支持plugins
|
||||
store => {
|
||||
initState = store.state;
|
||||
store.subscribe((mut, state) => {
|
||||
expect(state).toBe(state);
|
||||
mutations.push(mut);
|
||||
});
|
||||
store.subscribeAction(subscribeActionSpy);
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"module": "./esm/pinia-adapter.js",
|
||||
"main": "./cjs/pinia-adapter.js",
|
||||
"types": "./@types/index.d.ts"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"module": "./esm/vuex-adapter.js",
|
||||
"main": "./cjs/vuex-adapter.js",
|
||||
"types": "./@types/index.d.ts"
|
||||
}
|
|
@ -2,17 +2,17 @@
|
|||
"name": "@inula/vue-adapter",
|
||||
"version": "0.0.1",
|
||||
"description": "vue adapter",
|
||||
"main": "./build/cjs/vue-adapter.js",
|
||||
"module": "./build/esm/vue-adapter.js",
|
||||
"types": "build/@types/index.d.ts",
|
||||
"main": "./vue/cjs/vue-adapter.js",
|
||||
"module": "./vue/esm/vue-adapter.js",
|
||||
"types": "./vue/@types/index.d.ts",
|
||||
"files": [
|
||||
"/build",
|
||||
"build/**/*",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest --ui",
|
||||
"build": "rollup -c ./scripts/rollup.config.js && npm run build-types",
|
||||
"build-types": "tsc -p tsconfig.build.json && rollup -c ./scripts/build-types.js"
|
||||
"build-types": "tsc -p tsconfig.vue.json && tsc -p tsconfig.pinia.json && tsc -p tsconfig.vuex.json && rollup -c ./scripts/build-types.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"openinula": "workspace:*"
|
||||
|
@ -53,7 +53,8 @@
|
|||
"@vitest/ui": "^0.34.5",
|
||||
"jsdom": "^24.0.0",
|
||||
"vitest": "^0.34.5",
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@testing-library/user-event": "^12.1.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openinula": ">=0.1.1"
|
|
@ -1,18 +0,0 @@
|
|||
接口差异:
|
||||
|
||||
1、$patch不需要,可以直接赋值
|
||||
```ts
|
||||
actions: {
|
||||
setFoo(foo) {
|
||||
// not support
|
||||
this.$patch({ nested: { foo } });
|
||||
// support
|
||||
this.nested = { foo };
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
2、watch写法不同
|
||||
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"name": "@inula/pinia-adapter",
|
||||
"version": "0.0.1",
|
||||
"description": "pinia adapter",
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
"test": "vitest --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "^7.23.9",
|
||||
"@babel/core": "^7.23.9",
|
||||
"@babel/plugin-syntax-typescript": "^7.23.3",
|
||||
"openinula": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@vitest/ui": "^0.34.5",
|
||||
"jsdom": "^24.0.0",
|
||||
"vitest": "^0.34.5"
|
||||
}
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
import { createStore, watch as wt, StoreObj } from 'openinula';
|
||||
import { ref } from './ref';
|
||||
|
||||
const storeMap = new Map();
|
||||
|
||||
interface StoreDefinition {
|
||||
id: string;
|
||||
state: () => Record<string, any>;
|
||||
actions?: Record<string, Function>;
|
||||
getters?: Record<string, Function>;
|
||||
}
|
||||
|
||||
type StoreCallback = () => Record<string, any>;
|
||||
|
||||
export function defineStore(id: string | StoreDefinition, cbOrDef?: StoreCallback | StoreDefinition): Function {
|
||||
if (!cbOrDef && typeof id === 'object') {
|
||||
cbOrDef = id as StoreDefinition;
|
||||
id = cbOrDef.id;
|
||||
}
|
||||
|
||||
if (typeof cbOrDef === 'object') {
|
||||
return defineOptionsStore(id as string, cbOrDef as StoreDefinition);
|
||||
}
|
||||
|
||||
return () => {
|
||||
const data = (cbOrDef as StoreCallback)();
|
||||
|
||||
if (storeMap.has(cbOrDef)) {
|
||||
return storeMap.get(cbOrDef)();
|
||||
}
|
||||
|
||||
const entries = Object.entries(data);
|
||||
|
||||
const state = Object.fromEntries(entries.filter(([key, val]) => val.isRef).map(([key, val]) => [key, val.value]));
|
||||
const computed = Object.fromEntries(
|
||||
entries.filter(([key, val]) => val.isComputed).map(([key, val]) => [key, val.raw])
|
||||
);
|
||||
const actions = Object.fromEntries(entries.filter(([key, val]) => !val.isRef && !val.isComputed));
|
||||
|
||||
const useStore = createStore({
|
||||
id: id as string,
|
||||
state,
|
||||
computed,
|
||||
actions: enhanceActions(actions),
|
||||
});
|
||||
|
||||
Object.entries(data)
|
||||
.filter(([key, val]) => val.isRef)
|
||||
.forEach(([key, val]) =>
|
||||
val.watch(newVal => {
|
||||
useStore().$s[key] = newVal;
|
||||
})
|
||||
);
|
||||
|
||||
storeMap.set(cbOrDef, useStore);
|
||||
|
||||
return useStore();
|
||||
};
|
||||
}
|
||||
|
||||
function enhanceActions(actions) {
|
||||
if (!actions) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(actions).map(([key, value]) => {
|
||||
return [
|
||||
key,
|
||||
function (store, ...args) {
|
||||
return value.bind(this)(...args);
|
||||
},
|
||||
];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function defineOptionsStore<S, A, C>(
|
||||
id: string,
|
||||
{ state, actions, getters }: StoreDefinition
|
||||
): () => StoreObj<any, any, any> {
|
||||
if (typeof state === 'function') {
|
||||
state = state();
|
||||
}
|
||||
let useStore = null;
|
||||
|
||||
return () => {
|
||||
if (!useStore) {
|
||||
useStore = createStore({
|
||||
id,
|
||||
state,
|
||||
actions: enhanceActions(actions),
|
||||
computed: getters,
|
||||
});
|
||||
}
|
||||
|
||||
return useStore();
|
||||
};
|
||||
}
|
||||
|
||||
export function mapStores(...stores) {
|
||||
const result = {};
|
||||
|
||||
stores.forEach(store => {
|
||||
const expandedStore = store();
|
||||
result[`${expandedStore.id}Store`] = () => expandedStore;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function storeToRefs(store) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(store.$s).map(([key, value]) => {
|
||||
return [key, ref(value)];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function createPinia() {
|
||||
const result = {
|
||||
install: app => {}, // do something?
|
||||
use: plugin => result,
|
||||
state: {}, //
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
export const watch = wt;
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { useState, useRef } from 'openinula';
|
||||
export function refBase(initialValue, update, state) {
|
||||
let listeners = new Set();
|
||||
|
||||
return new Proxy(
|
||||
{ value: initialValue },
|
||||
{
|
||||
get: (target, name) => {
|
||||
if (name === 'value' || name === 'current') {
|
||||
return state.current;
|
||||
}
|
||||
if (name === Symbol.toPrimitive) {
|
||||
return () => state.current;
|
||||
}
|
||||
if (name === 'isRef') return true;
|
||||
if (name === 'watch')
|
||||
return cb => {
|
||||
listeners.add(cb);
|
||||
};
|
||||
if (name === 'raw') return initialValue;
|
||||
},
|
||||
set: (target, name, value) => {
|
||||
if (name === 'value') {
|
||||
if (state.current === value) return true;
|
||||
state.current = value;
|
||||
update();
|
||||
Array.from(listeners.values()).forEach(listener => {
|
||||
listener(value);
|
||||
});
|
||||
return true;
|
||||
} else if (name === 'current') {
|
||||
if (state.current === value) return true;
|
||||
state.current = value;
|
||||
return true;
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function ref(initialValue) {
|
||||
let [b, r] = useState(false);
|
||||
let state = useRef(initialValue);
|
||||
|
||||
return refBase(initialValue, () => r(!b), state);
|
||||
}
|
||||
|
||||
export function ref(initialValue) {
|
||||
let [b, r] = useState(false);
|
||||
let state = useRef(initialValue);
|
||||
|
||||
return refBase(initialValue, () => r(!b), state);
|
||||
}
|
|
@ -1,153 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck For the compiled code.
|
||||
|
||||
import { describe, it, vi, expect } from 'vitest';
|
||||
import { defineStore, createPinia } from '../src/pinia';
|
||||
import { ref } from '../src/ref';
|
||||
|
||||
function expectType<T>(_value: T): void {}
|
||||
|
||||
describe('pinia getters', () => {
|
||||
const useStore = defineStore({
|
||||
id: 'main',
|
||||
state: () => ({
|
||||
name: 'Eduardo',
|
||||
}),
|
||||
getters: {
|
||||
upperCaseName(store) {
|
||||
return store.name.toUpperCase();
|
||||
},
|
||||
doubleName(): string {
|
||||
return this.upperCaseName;
|
||||
},
|
||||
composed(): string {
|
||||
return this.upperCaseName + ': ok';
|
||||
},
|
||||
arrowUpper: state => {
|
||||
// @ts-expect-error
|
||||
state.nope;
|
||||
state.name.toUpperCase();
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
o() {
|
||||
// @ts-expect-error it should type getters
|
||||
this.arrowUpper.toUpperCase();
|
||||
this.o().toUpperCase();
|
||||
return 'a string';
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const useB = defineStore({
|
||||
id: 'B',
|
||||
state: () => ({ b: 'b' }),
|
||||
});
|
||||
|
||||
const useA = defineStore({
|
||||
id: 'A',
|
||||
state: () => ({ a: 'a' }),
|
||||
getters: {
|
||||
fromB(): string {
|
||||
const bStore = useB();
|
||||
return this.a + ' ' + bStore.b;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it('adds getters to the store', () => {
|
||||
const store = useStore();
|
||||
expect(store.upperCaseName).toBe('EDUARDO');
|
||||
|
||||
// @ts-expect-error
|
||||
store.nope;
|
||||
|
||||
store.name = 'Ed';
|
||||
expect(store.upperCaseName).toBe('ED');
|
||||
});
|
||||
|
||||
it('updates the value', () => {
|
||||
const store = useStore();
|
||||
store.name = 'Ed';
|
||||
expect(store.upperCaseName).toBe('ED');
|
||||
});
|
||||
|
||||
it('can use other getters', () => {
|
||||
const store = useStore();
|
||||
expect(store.composed).toBe('EDUARDO: ok');
|
||||
store.name = 'Ed';
|
||||
expect(store.composed).toBe('ED: ok');
|
||||
});
|
||||
|
||||
describe('cross used stores', () => {
|
||||
const useA = defineStore('a', () => {
|
||||
const n = ref(0);
|
||||
const double = computed(() => n.value * 2);
|
||||
const sum = computed(() => n.value + B.n);
|
||||
|
||||
function increment() {
|
||||
return n.value++;
|
||||
}
|
||||
|
||||
function incrementB() {
|
||||
return B.increment();
|
||||
}
|
||||
|
||||
return { n, double, sum, increment, incrementB };
|
||||
});
|
||||
|
||||
const useB = defineStore('b', () => {
|
||||
const n = ref(0);
|
||||
const double = computed(() => n.value * 2);
|
||||
|
||||
function increment() {
|
||||
return n.value++;
|
||||
}
|
||||
|
||||
function incrementA() {
|
||||
return A.increment();
|
||||
}
|
||||
|
||||
return { n, double, incrementA, increment };
|
||||
});
|
||||
|
||||
const A = useA();
|
||||
const B = useB();
|
||||
|
||||
it('keeps getters reactive', () => {
|
||||
const a = useA();
|
||||
const b = useB();
|
||||
|
||||
expectType<() => number>(a.increment);
|
||||
expectType<() => number>(b.increment);
|
||||
expectType<() => number>(a.incrementB);
|
||||
expectType<() => number>(b.incrementA);
|
||||
|
||||
expect(a.double).toBe(0);
|
||||
b.incrementA();
|
||||
expect(a.double).toBe(2);
|
||||
a.increment();
|
||||
expect(a.double).toBe(4);
|
||||
|
||||
expect(b.double).toBe(0);
|
||||
a.incrementB();
|
||||
expect(b.double).toBe(2);
|
||||
b.increment();
|
||||
expect(b.double).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck For the compiled code.
|
||||
|
||||
import { beforeEach, describe, it, vi, expect } from 'vitest';
|
||||
import { createPinia, defineStore, computed } from '../src/pinia';
|
||||
|
||||
describe('pinia state', () => {
|
||||
beforeEach(() => {});
|
||||
|
||||
const useStore = defineStore('main', {
|
||||
state: () => ({
|
||||
name: 'Eduardo',
|
||||
counter: 0,
|
||||
nested: { n: 0 },
|
||||
}),
|
||||
});
|
||||
|
||||
it('can directly access state at the store level', () => {
|
||||
const store = useStore();
|
||||
expect(store.name).toBe('Eduardo');
|
||||
store.name = 'Ed';
|
||||
expect(store.name).toBe('Ed');
|
||||
});
|
||||
|
||||
it.skip('state is reactive', () => {
|
||||
const store = useStore();
|
||||
const upperCased = computed(() => store.name.toUpperCase());
|
||||
expect(upperCased.value).toBe('EDUARDO');
|
||||
store.name = 'Ed';
|
||||
expect(upperCased.value).toBe('ED');
|
||||
});
|
||||
|
||||
it('can be nested set on store.$s, not $state', () => {
|
||||
const store = useStore();
|
||||
|
||||
store.$s.nested.n = 3;
|
||||
|
||||
expect(store.nested.n).toBe(3);
|
||||
expect(store.$s.nested.n).toBe(3);
|
||||
});
|
||||
});
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck For the compiled code.
|
||||
|
||||
import { beforeEach, describe, it, vi, expect } from 'vitest';
|
||||
import { createPinia, defineStore, computed } from '../src/pinia';
|
||||
|
||||
describe('pinia state', () => {
|
||||
beforeEach(() => {});
|
||||
|
||||
const useStore = defineStore({
|
||||
id: 'main',
|
||||
state: () => ({
|
||||
a: true,
|
||||
nested: {
|
||||
foo: 'foo',
|
||||
a: { b: 'string' },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
it.skip('sets the initial state', () => {
|
||||
const store = useStore();
|
||||
store.$s;
|
||||
expect(store.$s).toEqual({
|
||||
a: true,
|
||||
nested: {
|
||||
foo: 'foo',
|
||||
a: { b: 'string' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('reuses a store', () => {
|
||||
const useStore = defineStore({ id: 'main' });
|
||||
expect(useStore()).toBe(useStore());
|
||||
});
|
||||
|
||||
it('can replace its state', () => {
|
||||
const store = useStore();
|
||||
const spy = vi.fn();
|
||||
store.$s.watch('a', () => {
|
||||
spy();
|
||||
});
|
||||
|
||||
expect(store.a).toBe(true);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
|
||||
store.a = false;
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
}
|
||||
}
|
|
@ -51,14 +51,14 @@ export function cleanUp(folders) {
|
|||
};
|
||||
}
|
||||
|
||||
function buildTypeConfig() {
|
||||
function buildTypeConfig(name) {
|
||||
return {
|
||||
input: ['./build/@types/index.d.ts'],
|
||||
input: [`./build/${name}/@types/${name}/index.d.ts`],
|
||||
output: {
|
||||
file: './build/@types/index.d.ts',
|
||||
file: `./build/${name}/@types/index.d.ts`,
|
||||
},
|
||||
plugins: [dts(), cleanUp(['./build/@types/'])],
|
||||
plugins: [dts(), cleanUp([`./build/${name}/@types/`])],
|
||||
};
|
||||
}
|
||||
|
||||
export default [buildTypeConfig()];
|
||||
export default [buildTypeConfig('vue'), buildTypeConfig('pinia'), buildTypeConfig('vuex')];
|
|
@ -15,7 +15,6 @@
|
|||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import babel from '@rollup/plugin-babel';
|
||||
import nodeResolve from '@rollup/plugin-node-resolve';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
|
@ -29,16 +28,16 @@ if (!fs.existsSync(outDir)) {
|
|||
fs.mkdirSync(outDir, { recursive: true });
|
||||
}
|
||||
|
||||
const getConfig = mode => {
|
||||
const getConfig = (mode, name) => {
|
||||
const prod = mode.startsWith('prod');
|
||||
const outputList = [
|
||||
{
|
||||
file: path.join(outDir, `cjs/vue-adapter.${prod ? 'min.' : ''}js`),
|
||||
file: path.join(outDir, `${name}/cjs/${name}-adapter.${prod ? 'min.' : ''}js`),
|
||||
sourcemap: 'true',
|
||||
format: 'cjs',
|
||||
},
|
||||
{
|
||||
file: path.join(outDir, `umd/vue-adapter.${prod ? 'min.' : ''}js`),
|
||||
file: path.join(outDir, `${name}/umd/${name}-adapter.${prod ? 'min.' : ''}js`),
|
||||
name: 'VueAdapter',
|
||||
sourcemap: 'true',
|
||||
format: 'umd',
|
||||
|
@ -46,13 +45,13 @@ const getConfig = mode => {
|
|||
];
|
||||
if (!prod) {
|
||||
outputList.push({
|
||||
file: path.join(outDir, 'esm/vue-adapter.js'),
|
||||
file: path.join(outDir, `${name}/esm/${name}-adapter.js`),
|
||||
sourcemap: 'true',
|
||||
format: 'esm',
|
||||
});
|
||||
}
|
||||
return {
|
||||
input: path.join(rootDir, '/src/index.ts'),
|
||||
input: path.join(rootDir, `/src/${name}/index.ts`),
|
||||
output: outputList,
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
|
@ -66,8 +65,49 @@ const getConfig = mode => {
|
|||
extensions,
|
||||
}),
|
||||
prod && terser(),
|
||||
name === 'vue'
|
||||
? copyFiles([
|
||||
{
|
||||
from: path.join(rootDir, 'package.json'),
|
||||
to: path.join(outDir, 'package.json'),
|
||||
},
|
||||
{
|
||||
from: path.join(rootDir, 'README.md'),
|
||||
to: path.join(outDir, 'README.md'),
|
||||
},
|
||||
])
|
||||
: copyFiles([
|
||||
{
|
||||
from: path.join(rootDir, `npm/${name}/package.json`),
|
||||
to: path.join(outDir, `${name}/package.json`),
|
||||
},
|
||||
]),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export default [getConfig('dev'), getConfig('prod')];
|
||||
function copyFiles(copyPairs) {
|
||||
return {
|
||||
name: 'copy-files',
|
||||
generateBundle() {
|
||||
copyPairs.forEach(({ from, to }) => {
|
||||
const destDir = path.dirname(to);
|
||||
// 判断目标文件夹是否存在
|
||||
if (!fs.existsSync(destDir)) {
|
||||
// 目标文件夹不存在,创建它
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
}
|
||||
fs.copyFileSync(from, to);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default [
|
||||
getConfig('dev', 'vue'),
|
||||
getConfig('prod', 'vue'),
|
||||
getConfig('dev', 'pinia'),
|
||||
getConfig('prod', 'pinia'),
|
||||
getConfig('dev', 'vuex'),
|
||||
getConfig('prod', 'vuex'),
|
||||
];
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
import { createStore, StoreObj, vueReactive } from 'openinula';
|
||||
import {
|
||||
FilterAction,
|
||||
FilterComputed,
|
||||
FilterState,
|
||||
StoreDefinition,
|
||||
StoreSetup,
|
||||
Store,
|
||||
AnyFunction,
|
||||
ActionType,
|
||||
StoreToRefsReturn,
|
||||
} from './types';
|
||||
|
||||
const { ref, isRef, toRef, isReactive, isReadonly } = vueReactive;
|
||||
|
||||
const storeMap = new Map<string, any>();
|
||||
|
||||
export function defineStore<
|
||||
Id extends string,
|
||||
S extends Record<string, unknown>,
|
||||
A extends Record<string, AnyFunction>,
|
||||
C extends Record<string, AnyFunction>,
|
||||
>(definition: StoreDefinition<Id, S, A, C>): (pinia?: any) => Store<S, A, C>;
|
||||
|
||||
export function defineStore<
|
||||
Id extends string,
|
||||
S extends Record<string, unknown>,
|
||||
A extends Record<string, AnyFunction>,
|
||||
C extends Record<string, AnyFunction>,
|
||||
>(id: Id, definition: Omit<StoreDefinition<Id, S, A, C>, 'id'>): (pinia?: any) => Store<S, A, C>;
|
||||
|
||||
export function defineStore<Id extends string, SS extends Record<any, unknown>>(
|
||||
id: Id,
|
||||
setup: StoreSetup<SS>
|
||||
): (pinia?: any) => Store<FilterState<SS>, FilterAction<SS>, FilterComputed<SS>>;
|
||||
|
||||
export function defineStore(idOrDef: any, setupOrDef?: any) {
|
||||
let id: string;
|
||||
let definition: StoreDefinition | StoreSetup;
|
||||
let isSetup = false;
|
||||
|
||||
if (typeof idOrDef === 'string') {
|
||||
isSetup = typeof setupOrDef === 'function';
|
||||
id = idOrDef;
|
||||
definition = setupOrDef;
|
||||
} else {
|
||||
id = idOrDef.id;
|
||||
definition = idOrDef;
|
||||
}
|
||||
|
||||
if (isSetup) {
|
||||
return defineSetupStore(id, definition as StoreSetup);
|
||||
} else {
|
||||
return defineOptionsStore(id, definition as StoreDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* createStore实现中会给actions增加第一个参数store,pinia不需要,所以需要去掉
|
||||
* @param actions
|
||||
*/
|
||||
function enhanceActions(
|
||||
actions?: ActionType<Record<string, AnyFunction>, Record<string, unknown>, Record<string, AnyFunction>>
|
||||
) {
|
||||
if (!actions) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(actions).map(([key, value]) => {
|
||||
return [
|
||||
key,
|
||||
function (this: StoreObj, state: Record<string, unknown>, ...args: any[]) {
|
||||
return value.bind(this)(...args);
|
||||
},
|
||||
];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function defineOptionsStore(id: string, definition: StoreDefinition) {
|
||||
const state = definition.state ? definition.state() : {};
|
||||
const computed = definition.getters || {};
|
||||
const actions = enhanceActions(definition.actions) || {};
|
||||
|
||||
return () => {
|
||||
if (storeMap.has(id)) {
|
||||
return storeMap.get(id)!();
|
||||
}
|
||||
|
||||
const useStore = createStore({
|
||||
id,
|
||||
state,
|
||||
actions,
|
||||
computed,
|
||||
});
|
||||
|
||||
storeMap.set(id, useStore);
|
||||
|
||||
return useStore();
|
||||
};
|
||||
}
|
||||
|
||||
function defineSetupStore<SS extends Record<string, unknown>>(id: string, storeSetup: StoreSetup<SS>) {
|
||||
return () => {
|
||||
const data = storeSetup();
|
||||
if (!data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (storeMap.has(id)) {
|
||||
return storeMap.get(id)!();
|
||||
}
|
||||
|
||||
const state: Record<string, unknown> = {};
|
||||
const actions: Record<string, AnyFunction> = {};
|
||||
const getters: Record<string, AnyFunction> = {};
|
||||
for (const key in data) {
|
||||
const prop = data[key];
|
||||
|
||||
if ((isRef(prop) && !isReadonly(prop)) || isReactive(prop)) {
|
||||
// state
|
||||
state[key] = prop;
|
||||
} else if (typeof prop === 'function') {
|
||||
// action
|
||||
actions[key] = prop as AnyFunction;
|
||||
} else if (isRef(prop) && isReadonly(prop)) {
|
||||
// getters
|
||||
getters[key] = (prop as any).fn as AnyFunction;
|
||||
}
|
||||
}
|
||||
|
||||
const useStore = createStore({
|
||||
id,
|
||||
state,
|
||||
computed: getters,
|
||||
actions: enhanceActions(actions),
|
||||
});
|
||||
|
||||
storeMap.set(id, useStore);
|
||||
|
||||
return useStore();
|
||||
};
|
||||
}
|
||||
|
||||
export function mapStores<
|
||||
S extends Record<string, unknown>,
|
||||
A extends Record<string, AnyFunction>,
|
||||
C extends Record<string, AnyFunction>,
|
||||
>(...stores: (() => Store<S, A, C>)[]): { [key: string]: () => Store<S, A, C> } {
|
||||
const result: { [key: string]: () => Store<S, A, C> } = {};
|
||||
|
||||
stores.forEach((store: () => Store<S, A, C>) => {
|
||||
const expandedStore = store();
|
||||
result[`${expandedStore.id}Store`] = () => expandedStore;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function storeToRefs<
|
||||
S extends Record<string, unknown>,
|
||||
A extends Record<string, AnyFunction>,
|
||||
C extends Record<string, AnyFunction>,
|
||||
>(store: Store<S, A, C>): StoreToRefsReturn<S, C> {
|
||||
const stateRefs = Object.fromEntries(
|
||||
Object.entries(store.$s || {}).map(([key, value]) => {
|
||||
return [key, ref(value)];
|
||||
})
|
||||
);
|
||||
|
||||
const getterRefs = Object.fromEntries(
|
||||
Object.entries(store.$config.computed || {}).map(([key, value]) => {
|
||||
const computeFn = (value as () => any).bind(store, store.$s);
|
||||
return [key, toRef(computeFn)];
|
||||
})
|
||||
);
|
||||
|
||||
return { ...stateRefs, ...getterRefs } as StoreToRefsReturn<S, C>;
|
||||
}
|
||||
|
||||
export function createPinia() {
|
||||
console.warn(
|
||||
`The pinia-adapter in Horizon does not support the createPinia interface. Please modify your code accordingly.`
|
||||
);
|
||||
|
||||
const result = {
|
||||
install: (app: any) => {},
|
||||
use: (plugin: any) => result,
|
||||
state: {},
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
// import type {RefType, StoreObj, UnwrapRef, UserActions, UserComputedValues, ComputedImpl} from 'openinula';
|
||||
import type { RefType, UnwrapRef, ComputedImpl } from 'openinula';
|
||||
|
||||
export type StoreSetup<R = Record<string, unknown>> = () => R;
|
||||
|
||||
export type AnyFunction = (...args: any[]) => any;
|
||||
|
||||
// defineStore init type
|
||||
export interface StoreDefinition<
|
||||
Id extends string = string,
|
||||
S extends Record<string, unknown> = Record<string, unknown>,
|
||||
A extends Record<string, AnyFunction> = Record<string, AnyFunction>,
|
||||
C extends Record<string, AnyFunction> = Record<string, AnyFunction>,
|
||||
> {
|
||||
id?: Id;
|
||||
state?: () => S;
|
||||
actions?: ActionType<A, S, C>;
|
||||
getters?: ComputedType<C, S>;
|
||||
}
|
||||
|
||||
// defineStore return type
|
||||
export type Store<
|
||||
S extends Record<string, unknown>,
|
||||
A extends Record<string, AnyFunction>,
|
||||
C extends Record<string, AnyFunction>,
|
||||
> = {
|
||||
$s: S;
|
||||
$state: S;
|
||||
$a: ActionType<A, S, C>;
|
||||
$c: ComputedType<C, S>;
|
||||
$subscribe: (listener: Listener) => void;
|
||||
$unsubscribe: (listener: Listener) => void;
|
||||
} & { [K in keyof S]: S[K] } & { [K in keyof ActionType<A, S, C>]: ActionType<A, S, C>[K] } & {
|
||||
[K in keyof ComputedType<C, S>]: ReturnType<ComputedType<C, S>[K]>;
|
||||
};
|
||||
|
||||
export type ActionType<A, S, C> = A & ThisType<A & UnwrapRef<S> & WithGetters<C>>;
|
||||
|
||||
type ComputedType<C, S> = {
|
||||
[K in keyof C]: AddFirstArg<C[K], S>;
|
||||
} & ThisType<UnwrapRef<S> & WithGetters<C>>;
|
||||
type AddFirstArg<T, S> = T extends (...args: infer A) => infer R
|
||||
? (state: S, ...args: A) => R
|
||||
: T extends () => infer R
|
||||
? (state: S) => R
|
||||
: T;
|
||||
|
||||
// In Getter function, make this.xx can refer to other getters
|
||||
export type WithGetters<G> = {
|
||||
readonly [k in keyof G]: G[k] extends (...args: any[]) => infer R ? R : UnwrapRef<G[k]>;
|
||||
};
|
||||
|
||||
type Listener = (change: any) => void;
|
||||
|
||||
// Filter state properties
|
||||
export type FilterState<T extends Record<string, unknown>> = {
|
||||
[K in FilterStateProperties<T>]: UnwrapRef<T[K]>;
|
||||
};
|
||||
type FilterStateProperties<T extends Record<string, unknown>> = {
|
||||
[K in keyof T]: T[K] extends ComputedImpl
|
||||
? never
|
||||
: T[K] extends RefType
|
||||
? K
|
||||
: T[K] extends Record<any, unknown> // Reactive类型
|
||||
? K
|
||||
: never;
|
||||
}[keyof T];
|
||||
|
||||
// Filter action properties
|
||||
export type FilterAction<T extends Record<string, unknown>> = {
|
||||
[K in FilterFunctionProperties<T>]: T[K] extends AnyFunction ? T[K] : never;
|
||||
};
|
||||
type FilterFunctionProperties<T extends Record<string, unknown>> = {
|
||||
[K in keyof T]: T[K] extends AnyFunction ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
// Filter computed properties
|
||||
export type FilterComputed<T extends Record<string, unknown>> = {
|
||||
[K in FilterComputedProperties<T>]: T[K] extends ComputedImpl<infer T> ? (T extends AnyFunction ? T : never) : never;
|
||||
};
|
||||
type FilterComputedProperties<T extends Record<string, unknown>> = {
|
||||
[K in keyof T]: T[K] extends ComputedImpl ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
export type StoreToRefsReturn<S extends Record<string, unknown>, C extends Record<string, AnyFunction>> = {
|
||||
[K in keyof S]: RefType<S[K]>;
|
||||
} & {
|
||||
[K in keyof ComputedType<C, S>]: Readonly<RefType<ReturnType<ComputedType<C, S>[K]>>>;
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
|
@ -13,6 +13,7 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
import { useEffect, useLayoutEffect, useRef } from 'openinula';
|
||||
import { FN } from './types';
|
||||
|
||||
// 用于存储组件是否已挂载的状态
|
||||
const useIsMounted = () => {
|
||||
|
@ -26,32 +27,32 @@ const useIsMounted = () => {
|
|||
return isMounted.current;
|
||||
};
|
||||
|
||||
export const onBeforeMount = (fn: () => void) => {
|
||||
export const onBeforeMount = (fn: FN) => {
|
||||
const isMounted = useIsMounted();
|
||||
if (!isMounted) {
|
||||
fn?.();
|
||||
}
|
||||
};
|
||||
|
||||
export function onMounted(fn: () => void) {
|
||||
export function onMounted(fn: FN) {
|
||||
useEffect(() => {
|
||||
fn?.();
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function onBeforeUpdated(fn: () => void) {
|
||||
export function onBeforeUpdated(fn: FN) {
|
||||
useEffect(() => {
|
||||
fn?.();
|
||||
});
|
||||
}
|
||||
|
||||
export function onUpdated(fn: () => void) {
|
||||
export function onUpdated(fn: FN) {
|
||||
useEffect(() => {
|
||||
fn?.();
|
||||
});
|
||||
}
|
||||
|
||||
export const onBeforeUnmount = (fn: () => void) => {
|
||||
export const onBeforeUnmount = (fn: FN) => {
|
||||
useLayoutEffect(() => {
|
||||
return () => {
|
||||
fn?.();
|
||||
|
@ -59,7 +60,7 @@ export const onBeforeUnmount = (fn: () => void) => {
|
|||
}, []);
|
||||
};
|
||||
|
||||
export function onUnmounted(fn: () => void) {
|
||||
export function onUnmounted(fn: FN) {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
fn?.();
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
|
@ -13,8 +13,4 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
export default {
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
},
|
||||
};
|
||||
export type FN = () => void;
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
export * from './vuex';
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
export type AnyFunction = (...args: any[]) => any;
|
||||
|
||||
export interface VuexStoreOptions<
|
||||
State extends Record<string, unknown> = Record<string, unknown>,
|
||||
Mutations extends Record<string, AnyFunction> = Record<string, AnyFunction>,
|
||||
Actions extends Record<string, AnyFunction> = Record<string, AnyFunction>,
|
||||
Getters extends Record<string, AnyFunction> = Record<string, AnyFunction>,
|
||||
RootState extends Record<string, unknown> = Record<string, unknown>,
|
||||
RootGetters extends Record<string, AnyFunction> = Record<string, AnyFunction>,
|
||||
Modules extends Record<string, Record<string, unknown>> = Record<string, Record<string, unknown>>,
|
||||
> {
|
||||
namespaced?: boolean;
|
||||
state?: State | (() => State);
|
||||
mutations?: MutationsType<Mutations, State>;
|
||||
actions?: ActionsType<Actions, State, Getters, RootState, RootGetters>;
|
||||
getters?: GettersType<State, Getters, State, Getters>;
|
||||
modules?: {
|
||||
[k in keyof Modules]: VuexStoreOptions<Modules[k]>;
|
||||
};
|
||||
}
|
||||
|
||||
type MutationsType<Mutations, State> = {
|
||||
[K in keyof Mutations]: AddFirstArg<Mutations[K], State>;
|
||||
};
|
||||
|
||||
type ActionsType<Actions, State, Getters, RootState, RootGetters> = {
|
||||
[K in keyof Actions]: AddFirstArg<
|
||||
Actions[K],
|
||||
{
|
||||
commit: CommitType;
|
||||
dispatch: DispatchType;
|
||||
state: State;
|
||||
getters: Getters;
|
||||
rootState: RootState;
|
||||
rootGetters: RootGetters;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
type AddFirstArg<T, S> = T extends (arg1: any, ...args: infer A) => infer R
|
||||
? (state: S, ...args: A) => R
|
||||
: T extends () => infer R
|
||||
? (state: S) => R
|
||||
: T;
|
||||
|
||||
type GettersType<State, Getters, RootState, RootGetters> = {
|
||||
[K in keyof Getters]: AddArgs<Getters[K], [State, Getters, RootState, RootGetters]>;
|
||||
};
|
||||
|
||||
type AddArgs<T, Args extends any[]> = T extends (...args: infer A) => infer R
|
||||
? (...args: [...Args, ...A]) => R
|
||||
: T extends () => infer R
|
||||
? (...args: Args) => R
|
||||
: T;
|
||||
|
||||
export type CommitType = (
|
||||
type: string | (Record<string, unknown> & { type: string }),
|
||||
payload?: any,
|
||||
options?: Record<string, unknown>,
|
||||
moduleName?: string
|
||||
) => void;
|
||||
|
||||
export type DispatchType = (
|
||||
type: string | (Record<string, unknown> & { type: string }),
|
||||
payload?: any,
|
||||
options?: Record<string, unknown>,
|
||||
moduleName?: string
|
||||
) => any;
|
||||
|
||||
export type VuexStore<
|
||||
State extends Record<string, unknown> = Record<string, unknown>,
|
||||
Getters extends Record<string, AnyFunction> = Record<string, AnyFunction>,
|
||||
Modules extends Record<string, Record<string, unknown>> = Record<string, Record<string, unknown>>,
|
||||
> = {
|
||||
state: State & {
|
||||
[K in keyof Modules]: Modules[K] extends { state: infer ModuleState } ? ModuleState : Modules[K];
|
||||
};
|
||||
getters: {
|
||||
[K in keyof Getters]: ReturnType<Getters[K]>;
|
||||
};
|
||||
commit: CommitType;
|
||||
dispatch: DispatchType;
|
||||
subscribe: AnyFunction;
|
||||
subscribeAction: AnyFunction;
|
||||
watch: (fn: (state: State, getters: Getters) => void, cb: AnyFunction) => void;
|
||||
registerModule: (moduleName: string, module: VuexStoreOptions) => void;
|
||||
unregisterModule: (moduleName: string) => void;
|
||||
hasModule: (moduleName: string) => boolean;
|
||||
};
|
|
@ -0,0 +1,315 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { createStore as createStoreX, StoreObj, vueReactive } from 'openinula';
|
||||
import { VuexStore, VuexStoreOptions } from './types';
|
||||
import { AnyFunction } from '../pinia/types';
|
||||
|
||||
const { watch } = vueReactive;
|
||||
|
||||
const MUTATION_PREFIX = 'm_';
|
||||
const GETTER_PREFIX = 'g_';
|
||||
|
||||
type GettersMap<T extends StoreObj = StoreObj> = {
|
||||
[K in keyof T['$c']]: ReturnType<T['$c'][K]>;
|
||||
};
|
||||
|
||||
export function createStore<
|
||||
State extends Record<string, unknown> = Record<string, unknown>,
|
||||
Mutations extends Record<string, AnyFunction> = Record<string, AnyFunction>,
|
||||
Actions extends Record<string, AnyFunction> = Record<string, AnyFunction>,
|
||||
Getters extends Record<string, AnyFunction> = Record<string, AnyFunction>,
|
||||
RootState extends Record<string, unknown> = Record<string, unknown>,
|
||||
RootGetters extends Record<string, AnyFunction> = Record<string, AnyFunction>,
|
||||
Modules extends Record<string, Record<string, unknown>> = Record<string, Record<string, unknown>>,
|
||||
>(
|
||||
options: VuexStoreOptions<State, Mutations, Actions, Getters, RootState, RootGetters, Modules>
|
||||
): VuexStore<State, Getters, Modules> {
|
||||
const modules = options.modules || {};
|
||||
|
||||
const _modules: Record<string, { storeX: StoreObj; namespaced: boolean }> = {};
|
||||
|
||||
const _getters: GettersMap = {};
|
||||
|
||||
const vuexStore: VuexStore = {
|
||||
state: new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_, key) => {
|
||||
if (key in _modules) {
|
||||
return _modules[key as string].storeX;
|
||||
} else {
|
||||
return rootStoreX[key as string];
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
getters: new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_, key) => {
|
||||
if (typeof key === 'string') {
|
||||
// 如果key包含/,说明是访问模块的getters,进行split
|
||||
if (key.includes('/')) {
|
||||
const [moduleName, getterKey] = key.split('/');
|
||||
return _modules[moduleName].storeX[`${GETTER_PREFIX}${getterKey}`];
|
||||
} else {
|
||||
return _getters[`${GETTER_PREFIX}${key}`];
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
commit: (_type, _payload, _options, moduleName) => {
|
||||
const { type, payload, options } = prepareTypeParams(_type, _payload, _options);
|
||||
// 如果options.root为true,调用根store的action
|
||||
if (options?.root) {
|
||||
return rootStoreX[`${MUTATION_PREFIX}${type}`](payload);
|
||||
}
|
||||
|
||||
// 包含/,说明是访问模块的mutation
|
||||
if (type.includes('/')) {
|
||||
const [moduleName, key] = type.split('/');
|
||||
return _modules[moduleName].storeX[`${MUTATION_PREFIX}${key}`](payload);
|
||||
}
|
||||
|
||||
if (moduleName != undefined) {
|
||||
// dispatch到指定的module
|
||||
return _modules[moduleName].storeX[`${MUTATION_PREFIX}${type}`](payload);
|
||||
}
|
||||
|
||||
// 调用所有非namespaced的modules的mutation
|
||||
Object.values(_modules).forEach(module => {
|
||||
if (!module.namespaced) {
|
||||
const mutation = module.storeX[`${MUTATION_PREFIX}${type}`];
|
||||
if (typeof mutation === 'function') {
|
||||
mutation(payload);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 调用storeX对象上的方法
|
||||
if (rootStoreX[`${MUTATION_PREFIX}${type}`]) {
|
||||
rootStoreX[`${MUTATION_PREFIX}${type}`](payload);
|
||||
}
|
||||
},
|
||||
dispatch: (_type, _payload, _options, moduleName) => {
|
||||
const { type, payload, options } = prepareTypeParams(_type, _payload, _options);
|
||||
// 如果options.root为true,调用根store的action
|
||||
if (options?.root) {
|
||||
return rootStoreX[type](payload);
|
||||
}
|
||||
|
||||
// 包含/,说明是访问模块的action
|
||||
if (type.includes('/')) {
|
||||
const [moduleName, key] = type.split('/');
|
||||
return _modules[moduleName].storeX[key](payload);
|
||||
}
|
||||
|
||||
if (moduleName != undefined) {
|
||||
// dispatch到指定的module
|
||||
return _modules[moduleName].storeX[type](payload);
|
||||
}
|
||||
|
||||
// 把每个action的返回值合并起来,支持then链式调用
|
||||
const results: any[] = [];
|
||||
|
||||
// 调用所有非namespaced的modules的action
|
||||
Object.values(_modules).forEach(module => {
|
||||
if (!module.namespaced) {
|
||||
const action = module.storeX[type];
|
||||
if (typeof action === 'function') {
|
||||
results.push(action(payload));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 调用storeX对象上的方法
|
||||
if (typeof rootStoreX[type] === 'function') {
|
||||
results.push(rootStoreX[type](payload));
|
||||
}
|
||||
|
||||
// 返回一个Promise,内容是results,支持then链式调用
|
||||
return Promise.all(results);
|
||||
},
|
||||
subscribe(fn) {
|
||||
return rootStoreX.$subscribe(fn);
|
||||
},
|
||||
subscribeAction(fn) {
|
||||
return rootStoreX.$subscribe(fn);
|
||||
},
|
||||
watch(fn, cb) {
|
||||
watch(() => fn(vuexStore.state, vuexStore.getters), cb);
|
||||
},
|
||||
// 动态注册模块
|
||||
registerModule(key, module) {
|
||||
_modules[key] = { storeX: _createStoreX(key, module, vuexStore, rootStoreX), namespaced: !!module.namespaced };
|
||||
collectGetters(_modules[key].storeX, _getters);
|
||||
},
|
||||
// 动态注销模块
|
||||
unregisterModule(key) {
|
||||
deleteGetters(_modules[key].storeX, _getters);
|
||||
delete _modules[key];
|
||||
},
|
||||
hasModule(path) {
|
||||
return path in _modules;
|
||||
},
|
||||
};
|
||||
|
||||
const rootStoreX = _createStoreX(undefined, options as VuexStoreOptions, vuexStore);
|
||||
collectGetters(rootStoreX, _getters);
|
||||
|
||||
// 递归创建子模块
|
||||
for (const [moduleName, moduleOptions] of Object.entries(modules)) {
|
||||
_modules[moduleName] = {
|
||||
storeX: _createStoreX(moduleName, moduleOptions as VuexStoreOptions, vuexStore, rootStoreX),
|
||||
namespaced: !!(moduleOptions as VuexStoreOptions).namespaced,
|
||||
};
|
||||
collectGetters(_modules[moduleName].storeX, _getters);
|
||||
}
|
||||
|
||||
return vuexStore as VuexStore<State, Getters, Modules>;
|
||||
}
|
||||
|
||||
export function prepareTypeParams(
|
||||
type: string | (Record<string, unknown> & { type: string }),
|
||||
payload?: any,
|
||||
options?: Record<string, unknown>
|
||||
) {
|
||||
if (typeof type === 'object' && type.type) {
|
||||
options = payload;
|
||||
payload = type;
|
||||
type = type.type;
|
||||
}
|
||||
|
||||
return { type, payload, options } as {
|
||||
type: string;
|
||||
payload: any;
|
||||
options: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
function _createStoreX(
|
||||
moduleName: string | undefined,
|
||||
options: VuexStoreOptions,
|
||||
store: VuexStore,
|
||||
rootStoreX?: any
|
||||
): StoreObj {
|
||||
const { mutations = {}, actions = {}, getters = {} } = options;
|
||||
const state = typeof options.state === 'function' ? options.state() : options.state;
|
||||
|
||||
const storeX: StoreObj = createStoreX({
|
||||
id: moduleName,
|
||||
state: state,
|
||||
actions: {
|
||||
// 给mutations的key增加一个前缀,避免和actions的key冲突
|
||||
...Object.fromEntries(
|
||||
Object.entries(mutations).map(([key, mutation]) => {
|
||||
return [`${MUTATION_PREFIX}${key}`, mutation];
|
||||
})
|
||||
),
|
||||
// 重新定义action的方法,绑定this,修改第一参数
|
||||
...Object.fromEntries(
|
||||
Object.entries(actions).map(([key, action]) => [
|
||||
key,
|
||||
function (this: StoreObj, state: Record<string, unknown>, payload) {
|
||||
rootStoreX = rootStoreX || storeX;
|
||||
const argFirst = {
|
||||
...store,
|
||||
// 覆盖commit方法,多传一个参数moduleName
|
||||
commit: (
|
||||
type: string | (Record<string, unknown> & { type: string }),
|
||||
payload?: any,
|
||||
options?: Record<string, unknown>
|
||||
) => {
|
||||
store.commit(type, payload, options, moduleName);
|
||||
},
|
||||
// 覆盖dispatch方法,多传一个参数moduleName
|
||||
dispatch: (
|
||||
type: string | (Record<string, unknown> & { type: string }),
|
||||
payload?: any,
|
||||
options?: Record<string, unknown>
|
||||
) => {
|
||||
return store.dispatch(type, payload, options, moduleName);
|
||||
},
|
||||
state: storeX.$state,
|
||||
rootState: store.state,
|
||||
getter: store.getters,
|
||||
rootGetters: moduleGettersProxy(rootStoreX),
|
||||
};
|
||||
|
||||
return action.call(storeX, argFirst, payload);
|
||||
},
|
||||
])
|
||||
),
|
||||
},
|
||||
computed: {
|
||||
...Object.fromEntries(
|
||||
Object.entries(getters).map(([key, getter]) => {
|
||||
return [
|
||||
// 给getters的key增加一个前缀,避免和actions, mutations的key冲突
|
||||
`${GETTER_PREFIX}${key}`,
|
||||
// 重新定义getter的方法,绑定this,修改参数: state, getters, rootState, rootGetters
|
||||
function (state: Record<string, unknown>) {
|
||||
rootStoreX = rootStoreX || storeX;
|
||||
return getter.call(
|
||||
storeX,
|
||||
storeX.$state,
|
||||
store.getters,
|
||||
rootStoreX.$state,
|
||||
moduleGettersProxy(rootStoreX)
|
||||
);
|
||||
},
|
||||
];
|
||||
})
|
||||
),
|
||||
},
|
||||
})();
|
||||
|
||||
return storeX;
|
||||
}
|
||||
|
||||
function collectGetters(storeX: StoreObj, gettersMap: GettersMap): void {
|
||||
Object.keys(storeX.$config.computed).forEach(type => {
|
||||
Object.defineProperty(gettersMap, type, {
|
||||
get: () => storeX.$c[type],
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deleteGetters(storeX: StoreObj, gettersMap: GettersMap): void {
|
||||
Object.keys(storeX.$config.computed).forEach(type => {
|
||||
// 删除Object.defineProperty定义的属性
|
||||
Object.defineProperty(gettersMap, type, {
|
||||
value: undefined,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
delete gettersMap[type];
|
||||
});
|
||||
}
|
||||
|
||||
function moduleGettersProxy(storeX: StoreObj) {
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_, key) => {
|
||||
return storeX[`${GETTER_PREFIX}${key as string}`];
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -12,80 +12,66 @@
|
|||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck For the compiled code.
|
||||
|
||||
import { describe, it, vi, expect } from 'vitest';
|
||||
import { defineStore } from '../src/pinia';
|
||||
import { describe, it, vi, expect, beforeEach } from 'vitest';
|
||||
import { defineStore } from '../../src/pinia/pinia';
|
||||
|
||||
describe('pinia state', () => {
|
||||
const useStore = () => {
|
||||
return defineStore({
|
||||
id: 'main',
|
||||
state: () => ({
|
||||
a: true,
|
||||
nested: {
|
||||
foo: 'foo',
|
||||
a: { b: 'string' },
|
||||
},
|
||||
}),
|
||||
getters: {
|
||||
nonA(): boolean {
|
||||
return !this.a;
|
||||
},
|
||||
otherComputed() {
|
||||
return this.nonA;
|
||||
},
|
||||
let id = 0;
|
||||
function createStore() {
|
||||
return defineStore({
|
||||
id: String(id++),
|
||||
state: () => ({
|
||||
a: true,
|
||||
nested: {
|
||||
foo: 'foo',
|
||||
a: { b: 'string' },
|
||||
},
|
||||
actions: {
|
||||
async getNonA() {
|
||||
return this.nonA;
|
||||
},
|
||||
simple() {
|
||||
this.toggle();
|
||||
return 'simple';
|
||||
},
|
||||
|
||||
toggle() {
|
||||
return (this.a = !this.a);
|
||||
},
|
||||
|
||||
setFoo(foo: string) {
|
||||
this.nested = { foo };
|
||||
},
|
||||
|
||||
combined() {
|
||||
this.toggle();
|
||||
this.setFoo('bar');
|
||||
},
|
||||
|
||||
throws() {
|
||||
throw new Error('fail');
|
||||
},
|
||||
|
||||
async rejects() {
|
||||
throw 'fail';
|
||||
},
|
||||
}),
|
||||
getters: {
|
||||
nonA(): boolean {
|
||||
return !this.a;
|
||||
},
|
||||
})();
|
||||
};
|
||||
|
||||
const useB = defineStore({
|
||||
id: 'B',
|
||||
state: () => ({ b: 'b' }),
|
||||
});
|
||||
|
||||
const useA = defineStore({
|
||||
id: 'A',
|
||||
state: () => ({ a: 'a' }),
|
||||
actions: {
|
||||
swap() {
|
||||
const bStore = useB();
|
||||
const b = bStore.$state.b;
|
||||
bStore.$state.b = this.$state.a;
|
||||
this.$state.a = b;
|
||||
otherComputed() {
|
||||
return this.nonA;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async getNonA() {
|
||||
return this.nonA;
|
||||
},
|
||||
simple() {
|
||||
this.toggle();
|
||||
return 'simple';
|
||||
},
|
||||
|
||||
toggle() {
|
||||
return (this.a = !this.a);
|
||||
},
|
||||
|
||||
setFoo(foo: string) {
|
||||
this.nested.foo = foo;
|
||||
},
|
||||
|
||||
combined() {
|
||||
this.toggle();
|
||||
this.setFoo('bar');
|
||||
},
|
||||
|
||||
throws() {
|
||||
throw new Error('fail');
|
||||
},
|
||||
|
||||
async rejects() {
|
||||
throw 'fail';
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
describe('pinia state', () => {
|
||||
let useStore = createStore();
|
||||
|
||||
beforeEach(() => {
|
||||
useStore = createStore();
|
||||
});
|
||||
|
||||
it('can use the store as this', () => {
|
||||
|
@ -144,7 +130,6 @@ describe('pinia state', () => {
|
|||
// override the function like devtools do
|
||||
expect(
|
||||
{
|
||||
$id: store.$id,
|
||||
simple,
|
||||
// otherwise it would fail
|
||||
toggle() {},
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { defineStore } from '../../src/pinia/pinia';
|
||||
|
||||
let id = 0;
|
||||
function createStore() {
|
||||
return defineStore({
|
||||
id: String(id++),
|
||||
state: () => ({
|
||||
name: 'Eduardo',
|
||||
}),
|
||||
getters: {
|
||||
upperCaseName(store) {
|
||||
return store.name.toUpperCase();
|
||||
},
|
||||
doubleName(): string {
|
||||
return this.upperCaseName;
|
||||
},
|
||||
composed(): string {
|
||||
return this.upperCaseName + ': ok';
|
||||
},
|
||||
arrowUpper: state => {
|
||||
// @ts-expect-error
|
||||
state.nope;
|
||||
return state.name.toUpperCase();
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
o() {
|
||||
this.arrowUpper.toUpperCase();
|
||||
this.o().toUpperCase();
|
||||
return 'a string';
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('pinia getters', () => {
|
||||
let useStore = createStore();
|
||||
let useB;
|
||||
let useA;
|
||||
beforeEach(() => {
|
||||
useStore = createStore();
|
||||
|
||||
useB = defineStore({
|
||||
id: 'B',
|
||||
state: () => ({ b: 'b' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('adds getters to the store', () => {
|
||||
const store = useStore();
|
||||
expect(store.upperCaseName).toBe('EDUARDO');
|
||||
|
||||
// @ts-expect-error
|
||||
store.nope;
|
||||
|
||||
store.name = 'Ed';
|
||||
expect(store.upperCaseName).toBe('ED');
|
||||
});
|
||||
|
||||
it('updates the value', () => {
|
||||
const store = useStore();
|
||||
store.name = 'Ed';
|
||||
expect(store.upperCaseName).toBe('ED');
|
||||
});
|
||||
|
||||
it('can use other getters', () => {
|
||||
const store = useStore();
|
||||
expect(store.composed).toBe('EDUARDO: ok');
|
||||
store.name = 'Ed';
|
||||
expect(store.composed).toBe('ED: ok');
|
||||
});
|
||||
|
||||
it('keeps getters reactive when hydrating', () => {
|
||||
const store = useStore();
|
||||
store.name = 'Jack';
|
||||
expect(store.name).toBe('Jack');
|
||||
expect(store.upperCaseName).toBe('JACK');
|
||||
store.name = 'Ed';
|
||||
expect(store.upperCaseName).toBe('ED');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, it, vi, expect } from 'vitest';
|
||||
import { createPinia, defineStore } from '../../src/pinia/pinia';
|
||||
import { vueReactive } from 'openinula';
|
||||
|
||||
const { watch, computed, ref, reactive } = vueReactive;
|
||||
|
||||
let id = 0;
|
||||
function createStore() {
|
||||
return defineStore(String(id++), {
|
||||
state: () => ({
|
||||
name: 'Eduardo',
|
||||
counter: 0,
|
||||
nested: { n: 0 },
|
||||
}),
|
||||
actions: {
|
||||
increment(state, amount) {
|
||||
this.counter += amount;
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
upperCased() {
|
||||
return this.name.toUpperCase();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
describe('pinia state', () => {
|
||||
let useStore = createStore();
|
||||
|
||||
beforeEach(() => {
|
||||
useStore = createStore();
|
||||
});
|
||||
|
||||
it('can directly access state at the store level', () => {
|
||||
const store = useStore();
|
||||
expect(store.name).toBe('Eduardo');
|
||||
store.name = 'Ed';
|
||||
expect(store.name).toBe('Ed');
|
||||
});
|
||||
|
||||
it('state is reactive', () => {
|
||||
const store = useStore();
|
||||
const upperCased = computed(() => store.name.toUpperCase());
|
||||
expect(upperCased.value).toBe('EDUARDO');
|
||||
store.name = 'Ed';
|
||||
expect(upperCased.value).toBe('ED');
|
||||
});
|
||||
|
||||
it('can be set on store', () => {
|
||||
const pinia = createPinia();
|
||||
const store = useStore(pinia);
|
||||
|
||||
store.name = 'a';
|
||||
|
||||
expect(store.name).toBe('a');
|
||||
expect(store.$state.name).toBe('a');
|
||||
});
|
||||
|
||||
it('can be set on store.$state', () => {
|
||||
const pinia = createPinia();
|
||||
const store = useStore(pinia);
|
||||
|
||||
store.$state.name = 'a';
|
||||
|
||||
expect(store.name).toBe('a');
|
||||
expect(store.$state.name).toBe('a');
|
||||
});
|
||||
|
||||
it('can be nested set on store', () => {
|
||||
const pinia = createPinia();
|
||||
const store = useStore(pinia);
|
||||
|
||||
store.nested.n = 3;
|
||||
|
||||
expect(store.nested.n).toBe(3);
|
||||
expect(store.$state.nested.n).toBe(3);
|
||||
});
|
||||
|
||||
it('can be nested set on store.$state', () => {
|
||||
const pinia = createPinia();
|
||||
const store = useStore(pinia);
|
||||
|
||||
store.$state.nested.n = 3;
|
||||
|
||||
expect(store.nested.n).toBe(3);
|
||||
expect(store.$state.nested.n).toBe(3);
|
||||
});
|
||||
|
||||
it('state can be watched', async () => {
|
||||
const store = useStore();
|
||||
const spy = vi.fn();
|
||||
watch(() => store.name, spy);
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
store.name = 'Ed';
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('state can be watched when a ref is given', async () => {
|
||||
const store = useStore();
|
||||
const spy = vi.fn();
|
||||
watch(() => store.name, spy);
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
const nameRef = ref('Ed');
|
||||
// @ts-expect-error
|
||||
store.$state.name = nameRef;
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('can be given a ref', () => {
|
||||
const pinia = createPinia();
|
||||
const store = useStore(pinia);
|
||||
|
||||
// @ts-expect-error
|
||||
store.$state.name = ref('Ed');
|
||||
|
||||
expect(store.name).toBe('Ed');
|
||||
expect(store.$state.name).toBe('Ed');
|
||||
|
||||
store.name = 'Other';
|
||||
expect(store.name).toBe('Other');
|
||||
expect(store.$state.name).toBe('Other');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, it, vi, expect } from 'vitest';
|
||||
import { defineStore } from '../../src/pinia/pinia';
|
||||
import { vueReactive } from 'openinula';
|
||||
|
||||
const { watch } = vueReactive;
|
||||
|
||||
let id = 0;
|
||||
function createStore() {
|
||||
return defineStore({
|
||||
id: String(id++),
|
||||
state: () => ({
|
||||
a: true,
|
||||
nested: {
|
||||
foo: 'foo',
|
||||
a: { b: 'string' },
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
describe('pinia state', () => {
|
||||
let useStore = createStore();
|
||||
|
||||
beforeEach(() => {
|
||||
useStore = createStore();
|
||||
});
|
||||
|
||||
it('reuses a store', () => {
|
||||
const useStore = defineStore({ id: String(id++) });
|
||||
expect(useStore()).toBe(useStore());
|
||||
});
|
||||
|
||||
it('works with id as first argument', () => {
|
||||
const useStore = defineStore(String(id++), {
|
||||
state: () => ({
|
||||
a: true,
|
||||
nested: {
|
||||
foo: 'foo',
|
||||
a: { b: 'string' },
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(useStore()).toBe(useStore());
|
||||
const useStoreEmpty = defineStore(String(id++), {});
|
||||
expect(useStoreEmpty()).toBe(useStoreEmpty());
|
||||
});
|
||||
|
||||
it('sets the initial state', () => {
|
||||
const store = useStore();
|
||||
expect(store.$state).toEqual({
|
||||
a: true,
|
||||
nested: {
|
||||
foo: 'foo',
|
||||
a: { b: 'string' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('can replace its state', () => {
|
||||
const store = useStore();
|
||||
const spy = vi.fn();
|
||||
watch(() => store.a, spy);
|
||||
expect(store.a).toBe(true);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
store.$state = {
|
||||
a: false,
|
||||
nested: {
|
||||
foo: 'bar',
|
||||
a: {
|
||||
b: 'hey',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(store.$state).toEqual({
|
||||
a: false,
|
||||
nested: {
|
||||
foo: 'bar',
|
||||
a: { b: 'hey' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('can be $unsubscribe', () => {
|
||||
const useStore = defineStore({
|
||||
id: 'main',
|
||||
state: () => ({ n: 0 }),
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
const spy = vi.fn();
|
||||
|
||||
store.$subscribe(spy);
|
||||
store.$state.n++;
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(useStore()).toBe(store);
|
||||
store.$unsubscribe(spy);
|
||||
store.$state.n++;
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, it, vi, expect } from 'vitest';
|
||||
import { defineStore } from '../../src/pinia/pinia';
|
||||
import { vueReactive } from 'openinula';
|
||||
|
||||
const { ref, watch, computed } = vueReactive;
|
||||
|
||||
function expectType<T>(_value: T): void {}
|
||||
describe('store with setup syntax', () => {
|
||||
function mainFn() {
|
||||
const name = ref('Eduardo');
|
||||
const counter = ref(0);
|
||||
|
||||
function increment(amount = 1) {
|
||||
counter.value += amount;
|
||||
}
|
||||
|
||||
const double = computed(() => counter.value * 2);
|
||||
|
||||
return { name, counter, increment, double };
|
||||
}
|
||||
|
||||
let id = 0;
|
||||
function createStore() {
|
||||
return defineStore(String(id++), mainFn);
|
||||
}
|
||||
|
||||
let useStore = createStore();
|
||||
|
||||
beforeEach(() => {
|
||||
useStore = createStore();
|
||||
});
|
||||
|
||||
it('should extract the $state', () => {
|
||||
const store = useStore();
|
||||
expectType<{ name: string; counter: number }>(store.$state);
|
||||
expect(store.$state).toEqual({ name: 'Eduardo', counter: 0 });
|
||||
expect(store.name).toBe('Eduardo');
|
||||
expect(store.counter).toBe(0);
|
||||
expect(store.double).toBe(0);
|
||||
store.increment();
|
||||
expect(store.counter).toBe(1);
|
||||
expect(store.double).toBe(2);
|
||||
expect(store.$state).toEqual({ name: 'Eduardo', counter: 1 });
|
||||
expect(store.$state).not.toHaveProperty('double');
|
||||
expect(store.$state).not.toHaveProperty('increment');
|
||||
});
|
||||
|
||||
it('can store a function', () => {
|
||||
const store = defineStore(String(id++), () => {
|
||||
const fn = ref(() => {});
|
||||
|
||||
function action() {}
|
||||
|
||||
return { fn, action };
|
||||
})();
|
||||
expectType<{ fn: () => void }>(store.$state);
|
||||
expect(store.$state).toEqual({ fn: expect.any(Function) });
|
||||
expect(store.fn).toEqual(expect.any(Function));
|
||||
store.action();
|
||||
});
|
||||
|
||||
it('can directly access state at the store level', () => {
|
||||
const store = useStore();
|
||||
|
||||
expect(store.name).toBe('Eduardo');
|
||||
store.name = 'Ed';
|
||||
expect(store.name).toBe('Ed');
|
||||
});
|
||||
|
||||
it('state is reactive', () => {
|
||||
const store = useStore();
|
||||
const upperCased = computed(() => store.name.toUpperCase());
|
||||
expect(upperCased.value).toBe('EDUARDO');
|
||||
store.name = 'Ed';
|
||||
expect(upperCased.value).toBe('ED');
|
||||
});
|
||||
|
||||
it('state can be watched', async () => {
|
||||
const store = useStore();
|
||||
const spy = vi.fn();
|
||||
watch(() => store.name, spy);
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
store.name = 'Ed';
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('state refs can be watched', async () => {
|
||||
const store = useStore();
|
||||
const spy = vi.fn();
|
||||
watch(() => store.name, spy);
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
const nameRef = ref('Ed');
|
||||
store.name = nameRef;
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('unwraps refs', () => {
|
||||
const name = ref('Eduardo');
|
||||
const counter = ref(0);
|
||||
const double = computed(() => {
|
||||
return counter.value * 2;
|
||||
});
|
||||
|
||||
// const pinia = createPinia();
|
||||
// setActivePinia(pinia);
|
||||
const useStore = defineStore({
|
||||
id: String(id++),
|
||||
state: () => ({
|
||||
name,
|
||||
counter,
|
||||
double,
|
||||
}),
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
|
||||
expect(store.name).toBe('Eduardo');
|
||||
expect(store.$state.name).toBe('Eduardo');
|
||||
expect(store.$state).toEqual({
|
||||
name: 'Eduardo',
|
||||
counter: 0,
|
||||
double: 0,
|
||||
});
|
||||
|
||||
name.value = 'Ed';
|
||||
expect(store.name).toBe('Ed');
|
||||
expect(store.$state.name).toBe('Ed');
|
||||
|
||||
store.$state.name = 'Edu';
|
||||
expect(store.name).toBe('Edu');
|
||||
|
||||
// store.$patch({ counter: 2 });
|
||||
store.counter = 2;
|
||||
expect(store.counter).toBe(2);
|
||||
expect(counter.value).toBe(2);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, it, vi, expect } from 'vitest';
|
||||
import { defineStore, storeToRefs } from '../../src/pinia/pinia';
|
||||
import { vueReactive } from 'openinula';
|
||||
|
||||
const { ref, computed, reactive } = vueReactive;
|
||||
|
||||
let id = 0;
|
||||
describe('storeToRefs', () => {
|
||||
beforeEach(() => {});
|
||||
|
||||
function objectOfRefs<O extends Record<any, any>>(o: O) {
|
||||
return Object.keys(o).reduce((newO, key) => {
|
||||
// @ts-expect-error: we only need to match
|
||||
newO[key] = expect.objectContaining({ value: o[key] });
|
||||
return newO;
|
||||
}, {});
|
||||
}
|
||||
|
||||
it('empty state', () => {
|
||||
expect(storeToRefs(defineStore(String(id++), {})())).toEqual({});
|
||||
expect(storeToRefs(defineStore({ id: String(id++) })())).toEqual({});
|
||||
});
|
||||
|
||||
it('plain values', () => {
|
||||
const store = defineStore(String(id++), {
|
||||
state: () => ({ a: null as null | undefined, b: false, c: 1, d: 'd' }),
|
||||
})();
|
||||
|
||||
const { a, b, c, d } = storeToRefs(store);
|
||||
|
||||
expect(a.value).toBe(null);
|
||||
expect(b.value).toBe(false);
|
||||
expect(c.value).toBe(1);
|
||||
expect(d.value).toBe('d');
|
||||
|
||||
a.value = undefined;
|
||||
expect(a.value).toBe(undefined);
|
||||
|
||||
b.value = true;
|
||||
expect(b.value).toBe(true);
|
||||
|
||||
c.value = 2;
|
||||
expect(c.value).toBe(2);
|
||||
|
||||
d.value = 'e';
|
||||
expect(d.value).toBe('e');
|
||||
});
|
||||
|
||||
it('setup store', () => {
|
||||
const store = defineStore(String(id++), () => {
|
||||
return {
|
||||
a: ref<null | undefined>(null),
|
||||
b: ref(false),
|
||||
c: ref(1),
|
||||
d: ref('d'),
|
||||
r: reactive({ n: 1 }),
|
||||
};
|
||||
})();
|
||||
|
||||
const { a, b, c, d, r } = storeToRefs(store);
|
||||
|
||||
expect(a.value).toBe(null);
|
||||
expect(b.value).toBe(false);
|
||||
expect(c.value).toBe(1);
|
||||
expect(d.value).toBe('d');
|
||||
expect(r.value).toEqual({ n: 1 });
|
||||
|
||||
a.value = undefined;
|
||||
expect(a.value).toBe(undefined);
|
||||
|
||||
b.value = true;
|
||||
expect(b.value).toBe(true);
|
||||
|
||||
c.value = 2;
|
||||
expect(c.value).toBe(2);
|
||||
|
||||
d.value = 'e';
|
||||
expect(d.value).toBe('e');
|
||||
|
||||
r.value.n++;
|
||||
expect(r.value).toEqual({ n: 2 });
|
||||
expect(store.r).toEqual({ n: 2 });
|
||||
store.r.n++;
|
||||
expect(r.value).toEqual({ n: 3 });
|
||||
expect(store.r).toEqual({ n: 3 });
|
||||
});
|
||||
|
||||
it('empty getters', () => {
|
||||
expect(
|
||||
storeToRefs(
|
||||
defineStore(String(id++), {
|
||||
state: () => ({ n: 0 }),
|
||||
})()
|
||||
)
|
||||
).toEqual(objectOfRefs({ n: 0 }));
|
||||
expect(
|
||||
storeToRefs(
|
||||
defineStore(String(id++), () => {
|
||||
return { n: ref(0) };
|
||||
})()
|
||||
)
|
||||
).toEqual(objectOfRefs({ n: 0 }));
|
||||
});
|
||||
|
||||
it('contains getters', () => {
|
||||
const refs = storeToRefs(
|
||||
defineStore(String(id++), {
|
||||
state: () => ({ n: 1 }),
|
||||
getters: {
|
||||
double: state => state.n * 2,
|
||||
},
|
||||
})()
|
||||
);
|
||||
expect(refs).toEqual(objectOfRefs({ n: 1, double: 2 }));
|
||||
|
||||
const setupRefs = storeToRefs(
|
||||
defineStore(String(id++), () => {
|
||||
const n = ref(1);
|
||||
const double = computed(() => n.value * 2);
|
||||
return { n, double };
|
||||
})()
|
||||
);
|
||||
|
||||
expect(setupRefs).toEqual(objectOfRefs({ n: 1, double: 2 }));
|
||||
});
|
||||
});
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
import { describe, it, vi, expect } from 'vitest';
|
||||
import { render, act, useState } from 'openinula';
|
||||
import { onBeforeUnmount, onUnmounted, onMounted, onBeforeMount, onUpdated } from '../src';
|
||||
import { onBeforeUnmount, onUnmounted, onMounted, onBeforeMount, onUpdated } from '../../src/vue/lifecycle';
|
||||
|
||||
describe('lifecycle', () => {
|
||||
it('should call the onBeforeMount', () => {
|
|
@ -0,0 +1,480 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { createStore } from '../../src/vuex/vuex';
|
||||
|
||||
const TEST = 'TEST';
|
||||
|
||||
describe('Modules', () => {
|
||||
it('dynamic module registration', () => {
|
||||
const store = createStore({
|
||||
modules: {
|
||||
foo: {
|
||||
state: { bar: 1 },
|
||||
mutations: { inc: state => state.bar++ },
|
||||
actions: { inc: ({ commit }) => commit('inc') },
|
||||
getters: { fooBar: state => state.bar },
|
||||
},
|
||||
one: {
|
||||
state: { a: 0 },
|
||||
mutations: {
|
||||
aaa(state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
store.registerModule('hi', {
|
||||
state: { a: 1 },
|
||||
mutations: { inc: state => state.a++ },
|
||||
actions: { incHi: ({ commit }) => commit('inc') },
|
||||
getters: { ga: state => state.a },
|
||||
});
|
||||
|
||||
// expect(store._mutations.inc.length).toBe(2);
|
||||
expect(store.state.hi.a).toBe(1);
|
||||
expect(store.getters.ga).toBe(1);
|
||||
|
||||
// assert initial modules work as expected after dynamic registration
|
||||
expect(store.state.foo.bar).toBe(1);
|
||||
expect(store.getters.fooBar).toBe(1);
|
||||
|
||||
// test dispatching actions defined in dynamic module
|
||||
store.dispatch('incHi');
|
||||
expect(store.state.hi.a).toBe(2);
|
||||
expect(store.getters.ga).toBe(2);
|
||||
|
||||
expect(store.state.foo.bar).toBe(1);
|
||||
expect(store.getters.fooBar).toBe(1);
|
||||
|
||||
// unregister
|
||||
store.unregisterModule('hi');
|
||||
expect(store.state.hi).toBeUndefined();
|
||||
expect(store.getters.ga).toBeUndefined();
|
||||
|
||||
// assert initial modules still work as expected after unregister
|
||||
store.dispatch('inc');
|
||||
expect(store.state.foo.bar).toBe(2);
|
||||
expect(store.getters.fooBar).toBe(2);
|
||||
});
|
||||
|
||||
it('dynamic module registration with namespace inheritance', () => {
|
||||
const store = createStore({
|
||||
modules: {
|
||||
a: {
|
||||
namespaced: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const actionSpy = vi.fn();
|
||||
const mutationSpy = vi.fn();
|
||||
store.registerModule('b', {
|
||||
state: { value: 1 },
|
||||
getters: { foo: state => state.value },
|
||||
actions: { foo: actionSpy },
|
||||
mutations: { foo: mutationSpy },
|
||||
});
|
||||
|
||||
expect(store.state.b.value).toBe(1);
|
||||
expect(store.getters['foo']).toBe(1);
|
||||
|
||||
store.dispatch('foo');
|
||||
expect(actionSpy).toHaveBeenCalled();
|
||||
|
||||
store.commit('foo');
|
||||
expect(mutationSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('dynamic module existance test', () => {
|
||||
const store = createStore({});
|
||||
|
||||
store.registerModule('bonjour', {});
|
||||
|
||||
expect(store.hasModule('bonjour')).toBe(true);
|
||||
store.unregisterModule('bonjour');
|
||||
expect(store.hasModule('bonjour')).toBe(false);
|
||||
});
|
||||
|
||||
it('should keep getters when component gets destroyed', async () => {
|
||||
const store = createStore({});
|
||||
|
||||
const spy = vi.fn();
|
||||
|
||||
const moduleA = {
|
||||
namespaced: true,
|
||||
state: () => ({ value: 1 }),
|
||||
getters: {
|
||||
getState(state) {
|
||||
spy();
|
||||
return state.value;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
increment: state => {
|
||||
state.value++;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
store.registerModule('moduleA', moduleA);
|
||||
|
||||
expect(store.getters['moduleA/getState']).toBe(1);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
|
||||
store.commit('moduleA/increment');
|
||||
|
||||
expect(store.getters['moduleA/getState']).toBe(2);
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should not fire an unrelated watcher', () => {
|
||||
const spy = vi.fn();
|
||||
const store = createStore({
|
||||
modules: {
|
||||
a: {
|
||||
state: { value: 1 },
|
||||
},
|
||||
b: {},
|
||||
},
|
||||
});
|
||||
|
||||
store.watch(state => state.a, spy);
|
||||
store.registerModule('c', {
|
||||
state: { value: 2 },
|
||||
});
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('state as function (multiple module in same store)', () => {
|
||||
const store = createStore({
|
||||
modules: {
|
||||
one: {
|
||||
state: { a: 0 },
|
||||
mutations: {
|
||||
[TEST](state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
},
|
||||
},
|
||||
two: {
|
||||
state() {
|
||||
return { a: 0 };
|
||||
},
|
||||
mutations: {
|
||||
[TEST](state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(store.state.one.a).toBe(0);
|
||||
expect(store.state.two.a).toBe(0);
|
||||
|
||||
store.commit(TEST, 1);
|
||||
expect(store.state.one.a).toBe(1);
|
||||
expect(store.state.two.a).toBe(1);
|
||||
});
|
||||
|
||||
it('state as function (same module in multiple stores)', () => {
|
||||
const storeA = createStore({
|
||||
modules: {
|
||||
foo: {
|
||||
state() {
|
||||
return { a: 0 };
|
||||
},
|
||||
mutations: {
|
||||
[TEST](state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const storeB = createStore({
|
||||
modules: {
|
||||
bar: {
|
||||
state() {
|
||||
return { a: 0 };
|
||||
},
|
||||
mutations: {
|
||||
[TEST](state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(storeA.state.foo.a).toBe(0);
|
||||
expect(storeB.state.bar.a).toBe(0);
|
||||
|
||||
storeA.commit(TEST, 1);
|
||||
expect(storeA.state.foo.a).toBe(1);
|
||||
expect(storeB.state.bar.a).toBe(0);
|
||||
|
||||
storeB.commit(TEST, 2);
|
||||
expect(storeA.state.foo.a).toBe(1);
|
||||
expect(storeB.state.bar.a).toBe(2);
|
||||
});
|
||||
|
||||
it('module: mutation', function () {
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 1,
|
||||
},
|
||||
mutations: {
|
||||
[TEST](state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
},
|
||||
modules: {
|
||||
nested: {
|
||||
state: { a: 2 },
|
||||
mutations: {
|
||||
[TEST](state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
},
|
||||
},
|
||||
four: {
|
||||
state: { a: 6 },
|
||||
mutations: {
|
||||
[TEST](state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
store.commit(TEST, 1);
|
||||
expect(store.state.a).toBe(2);
|
||||
expect(store.state.nested.a).toBe(3);
|
||||
expect(store.state.four.a).toBe(7);
|
||||
});
|
||||
|
||||
it('module: action', function () {
|
||||
let calls = 0;
|
||||
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 1,
|
||||
},
|
||||
actions: {
|
||||
[TEST]({ state, rootState }) {
|
||||
calls++;
|
||||
expect(state.a).toBe(1);
|
||||
expect(rootState).toBe(store.state);
|
||||
},
|
||||
},
|
||||
modules: {
|
||||
nested: {
|
||||
state: { a: 2 },
|
||||
actions: {
|
||||
[TEST]({ state, rootState }) {
|
||||
calls++;
|
||||
expect(state.a).toBe(2);
|
||||
expect(rootState).toBe(store.state);
|
||||
},
|
||||
},
|
||||
},
|
||||
four: {
|
||||
state: { a: 6 },
|
||||
actions: {
|
||||
[TEST]({ state, rootState }) {
|
||||
calls++;
|
||||
expect(state.a).toBe(6);
|
||||
expect(rootState).toBe(store.state);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
store.dispatch(TEST);
|
||||
expect(calls).toBe(3);
|
||||
});
|
||||
|
||||
it('module: getters', function () {
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 1,
|
||||
},
|
||||
getters: {
|
||||
constant: () => 0,
|
||||
[`getter1`]: (state, getters, rootState) => {
|
||||
expect(getters.constant).toBe(0);
|
||||
expect(rootState.a).toBe(store.state.a);
|
||||
return state.a;
|
||||
},
|
||||
},
|
||||
modules: {
|
||||
nested: {
|
||||
state: { a: 2 },
|
||||
getters: {
|
||||
[`getter2`]: (state, getters, rootState) => {
|
||||
expect(getters.constant).toBe(0);
|
||||
expect(rootState.a).toBe(store.state.a);
|
||||
return state.a;
|
||||
},
|
||||
},
|
||||
},
|
||||
four: {
|
||||
state: { a: 6 },
|
||||
getters: {
|
||||
[`getter6`]: (state, getters, rootState) => {
|
||||
expect(getters.constant).toBe(0);
|
||||
expect(rootState.a).toBe(store.state.a);
|
||||
return state.a;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
[1, 2, 6].forEach(n => {
|
||||
expect(store.getters[`getter${n}`]).toBe(n);
|
||||
});
|
||||
});
|
||||
|
||||
it('module: namespace', () => {
|
||||
const actionSpy = vi.fn();
|
||||
const mutationSpy = vi.fn();
|
||||
|
||||
const store = createStore({
|
||||
modules: {
|
||||
a: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
a: 1,
|
||||
},
|
||||
getters: {
|
||||
b: () => 2,
|
||||
},
|
||||
actions: {
|
||||
[TEST]: actionSpy,
|
||||
},
|
||||
mutations: {
|
||||
[TEST]: mutationSpy,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(store.state.a.a).toBe(1);
|
||||
expect(store.getters['a/b']).toBe(2);
|
||||
store.dispatch('a/' + TEST);
|
||||
expect(actionSpy).toHaveBeenCalled();
|
||||
store.commit('a/' + TEST);
|
||||
expect(mutationSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('module: getters are namespaced in namespaced module', () => {
|
||||
const store = createStore({
|
||||
state: { value: 'root' },
|
||||
getters: {
|
||||
foo: state => state.value,
|
||||
},
|
||||
modules: {
|
||||
a: {
|
||||
namespaced: true,
|
||||
state: { value: 'module' },
|
||||
getters: {
|
||||
foo: state => {
|
||||
return state.value;
|
||||
},
|
||||
bar: (state, getters) => {
|
||||
return getters.foo;
|
||||
},
|
||||
baz: (state, getters, rootState, rootGetters) => rootGetters.foo,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(store.getters['a/foo']).toBe('module');
|
||||
expect(store.getters['a/bar']).toBe('module');
|
||||
expect(store.getters['a/baz']).toBe('root');
|
||||
});
|
||||
|
||||
it('module: action context is namespaced in namespaced module', () => {
|
||||
const rootActionSpy = vi.fn();
|
||||
const rootMutationSpy = vi.fn();
|
||||
const moduleActionSpy = vi.fn();
|
||||
const moduleMutationSpy = vi.fn();
|
||||
|
||||
const store = createStore({
|
||||
state: { value: 'root' },
|
||||
getters: { foo: state => state.value },
|
||||
actions: { foo: rootActionSpy },
|
||||
mutations: { foo: rootMutationSpy },
|
||||
modules: {
|
||||
a: {
|
||||
namespaced: true,
|
||||
state: { value: 'module' },
|
||||
getters: { foo: state => state.value },
|
||||
actions: {
|
||||
foo: moduleActionSpy,
|
||||
test({ dispatch, commit, getters, rootGetters }) {
|
||||
expect(getters.foo).toBe('module');
|
||||
expect(rootGetters.foo).toBe('root');
|
||||
|
||||
dispatch('foo');
|
||||
expect(moduleActionSpy).toHaveBeenCalledTimes(1);
|
||||
dispatch('foo', null, { root: true });
|
||||
expect(rootActionSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
commit('foo');
|
||||
expect(moduleMutationSpy).toHaveBeenCalledTimes(1);
|
||||
commit('foo', null, { root: true });
|
||||
expect(rootMutationSpy).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
},
|
||||
mutations: { foo: moduleMutationSpy },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
store.dispatch('a/test');
|
||||
});
|
||||
|
||||
it('dispatching multiple actions in different modules', () => {
|
||||
const store = createStore({
|
||||
modules: {
|
||||
a: {
|
||||
actions: {
|
||||
[TEST]() {
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
},
|
||||
b: {
|
||||
actions: {
|
||||
[TEST]() {
|
||||
return new Promise(r => r(2));
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
store.dispatch(TEST).then(res => {
|
||||
expect(res[0]).toBe(1);
|
||||
expect(res[1]).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,335 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { createStore } from '../../src/vuex/vuex';
|
||||
|
||||
const TEST_M = 'TEST_M';
|
||||
const TEST_A = 'TEST_A';
|
||||
|
||||
describe('vuex Store', () => {
|
||||
it('committing mutations', () => {
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 1,
|
||||
},
|
||||
mutations: {
|
||||
[TEST_M](state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
},
|
||||
});
|
||||
store.commit(TEST_M, 2);
|
||||
expect(store.state.a).toBe(3);
|
||||
});
|
||||
|
||||
it('committing with object style', () => {
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 1,
|
||||
},
|
||||
mutations: {
|
||||
[TEST_M](state, payload) {
|
||||
state.a += payload.amount;
|
||||
},
|
||||
},
|
||||
});
|
||||
store.commit({
|
||||
type: TEST_M,
|
||||
amount: 2,
|
||||
});
|
||||
expect(store.state.a).toBe(3);
|
||||
});
|
||||
|
||||
it('dispatching actions, sync', () => {
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 1,
|
||||
},
|
||||
mutations: {
|
||||
[TEST_M](state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
[TEST_A]({ commit }, n) {
|
||||
commit(TEST_M, n);
|
||||
},
|
||||
},
|
||||
});
|
||||
store.dispatch(TEST_A, 2);
|
||||
expect(store.state.a).toBe(3);
|
||||
});
|
||||
|
||||
it('dispatching with object style', () => {
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 1,
|
||||
},
|
||||
mutations: {
|
||||
[TEST_M](state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
[TEST_A]({ commit }, payload) {
|
||||
commit(TEST_M, payload.amount);
|
||||
},
|
||||
},
|
||||
});
|
||||
store.dispatch({
|
||||
type: TEST_A,
|
||||
amount: 2,
|
||||
});
|
||||
expect(store.state.a).toBe(3);
|
||||
});
|
||||
|
||||
it('dispatching actions, with returned Promise', () => {
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 1,
|
||||
},
|
||||
mutations: {
|
||||
[TEST_M](state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
[TEST_A]({ commit }, n) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
commit(TEST_M, n);
|
||||
resolve('');
|
||||
}, 0);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(store.state.a).toBe(1);
|
||||
store.dispatch(TEST_A, 2).then(() => {
|
||||
expect(store.state.a).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
it('composing actions with async/await', () => {
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 1,
|
||||
},
|
||||
mutations: {
|
||||
[TEST_M](state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
[TEST_A]({ commit }, n) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
commit(TEST_M, n);
|
||||
resolve('');
|
||||
}, 0);
|
||||
});
|
||||
},
|
||||
two: async ({ commit, dispatch }, n) => {
|
||||
await dispatch(TEST_A, 1);
|
||||
expect(store.state.a).toBe(2);
|
||||
commit(TEST_M, n);
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(store.state.a).toBe(1);
|
||||
store.dispatch('two', 3).then(() => {
|
||||
expect(store.state.a).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
it('detecting action Promise errors', () => {
|
||||
const store = createStore({
|
||||
actions: {
|
||||
[TEST_A]() {
|
||||
return new Promise((resolve, reject) => {
|
||||
reject('no');
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
const thenSpy = vi.fn();
|
||||
store
|
||||
.dispatch(TEST_A)
|
||||
.then(thenSpy)
|
||||
.catch((err: string) => {
|
||||
expect(thenSpy).not.toHaveBeenCalled();
|
||||
expect(err).toBe('no');
|
||||
});
|
||||
});
|
||||
|
||||
it('getters', () => {
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 0,
|
||||
},
|
||||
getters: {
|
||||
state: state => (state.a > 0 ? 'hasAny' : 'none'),
|
||||
},
|
||||
mutations: {
|
||||
[TEST_M](state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
check({ getters }, value) {
|
||||
// check for exposing getters into actions
|
||||
expect(getters.state).toBe(value);
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(store.getters.state).toBe('none');
|
||||
store.dispatch('check', 'none');
|
||||
|
||||
store.commit(TEST_M, 1);
|
||||
|
||||
expect(store.getters.state).toBe('hasAny');
|
||||
store.dispatch('check', 'hasAny');
|
||||
});
|
||||
|
||||
it('should accept state as function', () => {
|
||||
const store = createStore({
|
||||
state: () => ({
|
||||
a: 1,
|
||||
}),
|
||||
mutations: {
|
||||
[TEST_M](state, n) {
|
||||
state.a += n;
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(store.state.a).toBe(1);
|
||||
store.commit(TEST_M, 2);
|
||||
expect(store.state.a).toBe(3);
|
||||
});
|
||||
|
||||
it('subscribe: should handle subscriptions / unsubscriptions', () => {
|
||||
const subscribeSpy = vi.fn();
|
||||
const secondSubscribeSpy = vi.fn();
|
||||
const testPayload = 2;
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 1,
|
||||
},
|
||||
mutations: {
|
||||
[TEST_M](state) {
|
||||
state.a++;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const unsubscribe = store.subscribe(subscribeSpy);
|
||||
store.subscribe(secondSubscribeSpy);
|
||||
store.commit(TEST_M, testPayload);
|
||||
unsubscribe();
|
||||
store.commit(TEST_M, testPayload);
|
||||
|
||||
expect(subscribeSpy).toHaveBeenCalledTimes(1);
|
||||
expect(secondSubscribeSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('subscribe: should handle subscriptions with synchronous unsubscriptions', () => {
|
||||
const subscribeSpy = vi.fn();
|
||||
const testPayload = 2;
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 1,
|
||||
},
|
||||
mutations: {
|
||||
[TEST_M](state) {
|
||||
state.a++;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const unsubscribe = store.subscribe(() => unsubscribe());
|
||||
store.subscribe(subscribeSpy);
|
||||
store.commit(TEST_M, testPayload);
|
||||
|
||||
expect(subscribeSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('subscribeAction: should handle subscriptions with synchronous unsubscriptions', () => {
|
||||
const subscribeSpy = vi.fn();
|
||||
const testPayload = 2;
|
||||
const store = createStore({
|
||||
state: {
|
||||
a: 1,
|
||||
},
|
||||
actions: {
|
||||
[TEST_A]({ state }) {
|
||||
state.a++;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const unsubscribe = store.subscribeAction(() => unsubscribe());
|
||||
store.subscribeAction(subscribeSpy);
|
||||
store.dispatch(TEST_A, testPayload);
|
||||
|
||||
expect(subscribeSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('watch: with resetting vm', () => {
|
||||
const store = createStore({
|
||||
state: {
|
||||
count: 0,
|
||||
},
|
||||
mutations: {
|
||||
[TEST_M]: state => state.count++,
|
||||
},
|
||||
});
|
||||
|
||||
const spy = vi.fn();
|
||||
store.watch(state => state.count, spy);
|
||||
|
||||
store.commit(TEST_M);
|
||||
expect(store.state.count).toBe(1);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("watch: getter function has access to store's getters object", () => {
|
||||
const store = createStore({
|
||||
state: {
|
||||
count: 0,
|
||||
},
|
||||
mutations: {
|
||||
[TEST_M]: state => state.count++,
|
||||
},
|
||||
getters: {
|
||||
getCount: state => state.count,
|
||||
},
|
||||
});
|
||||
|
||||
const getter = function getter(state: any) {
|
||||
return state.count;
|
||||
};
|
||||
const spy = vi.spyOn({ getter }, 'getter');
|
||||
const spyCb = vi.fn();
|
||||
|
||||
store.watch(spy as any, spyCb);
|
||||
|
||||
store.commit(TEST_M);
|
||||
expect(store.state.count).toBe(1);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(store.state, store.getters);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"files": [
|
||||
"src/pinia/index.ts"
|
||||
],
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": true,
|
||||
"declarationDir": "./build/pinia/@types"
|
||||
},
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"files": [
|
||||
"src/index.ts"
|
||||
"src/vue/index.ts"
|
||||
],
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": true,
|
||||
"declarationDir": "./build/@types"
|
||||
"declarationDir": "./build/vue/@types"
|
||||
},
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"files": [
|
||||
"src/vuex/index.ts"
|
||||
],
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": true,
|
||||
"declarationDir": "./build/vuex/@types"
|
||||
},
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
|
@ -15,7 +15,7 @@
|
|||
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
let alias = {
|
||||
const alias = {
|
||||
react: 'openinula', // 新增
|
||||
'react-dom': 'openinula', // 新增
|
||||
'react/jsx-dev-runtime': 'openinula/jsx-dev-runtime',
|
|
@ -1,2 +0,0 @@
|
|||
## 适配Vue
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { vueReactive } from '../../../src';
|
||||
|
||||
const { ref, isRef, reactive, unref, toRef, toRefs, isReadonly, watchEffect } = vueReactive;
|
||||
|
||||
describe('test toRef, toRefs', () => {
|
||||
it('toRef', () => {
|
||||
const a = reactive({
|
||||
x: 1,
|
||||
});
|
||||
const x = toRef(a, 'x');
|
||||
expect(isRef(x)).toBe(true);
|
||||
expect(x.value).toBe(1);
|
||||
|
||||
// source -> proxy
|
||||
a.x = 2;
|
||||
expect(x.value).toBe(2);
|
||||
|
||||
// proxy -> source
|
||||
x.value = 3;
|
||||
expect(a.x).toBe(3);
|
||||
|
||||
// reactivity
|
||||
let dummyX;
|
||||
watchEffect(() => {
|
||||
dummyX = x.value;
|
||||
});
|
||||
expect(dummyX).toBe(x.value);
|
||||
|
||||
// mutating source should trigger effect using the proxy refs
|
||||
a.x = 4;
|
||||
expect(dummyX).toBe(4);
|
||||
|
||||
// should keep ref
|
||||
const r = { x: ref(1) };
|
||||
expect(toRef(r, 'x')).toBe(r.x);
|
||||
});
|
||||
|
||||
it('toRef on array', () => {
|
||||
const a = reactive(['a', 'b']);
|
||||
const r = toRef(a, 1);
|
||||
expect(r.value).toBe('b');
|
||||
r.value = 'c';
|
||||
expect(r.value).toBe('c');
|
||||
expect(a[1]).toBe('c');
|
||||
});
|
||||
|
||||
it('toRef default value', () => {
|
||||
const a: { x: number | undefined } = { x: undefined };
|
||||
const x = toRef(a, 'x', 1);
|
||||
expect(x.value).toBe(1);
|
||||
|
||||
a.x = 2;
|
||||
expect(x.value).toBe(2);
|
||||
|
||||
a.x = undefined;
|
||||
expect(x.value).toBe(1);
|
||||
});
|
||||
|
||||
it('toRef getter', () => {
|
||||
const x = toRef(() => 1);
|
||||
expect(x.value).toBe(1);
|
||||
expect(isRef(x)).toBe(true);
|
||||
expect(unref(x)).toBe(1);
|
||||
//@ts-expect-error
|
||||
expect(() => (x.value = 123)).toThrow();
|
||||
|
||||
expect(isReadonly(x)).toBe(true);
|
||||
});
|
||||
|
||||
it('toRefs', () => {
|
||||
const a = reactive({
|
||||
x: 1,
|
||||
y: 2,
|
||||
});
|
||||
|
||||
const { x, y } = toRefs(a);
|
||||
|
||||
expect(isRef(x)).toBe(true);
|
||||
expect(isRef(y)).toBe(true);
|
||||
expect(x.value).toBe(1);
|
||||
expect(y.value).toBe(2);
|
||||
|
||||
// source -> proxy
|
||||
a.x = 2;
|
||||
a.y = 3;
|
||||
expect(x.value).toBe(2);
|
||||
expect(y.value).toBe(3);
|
||||
|
||||
// proxy -> source
|
||||
x.value = 3;
|
||||
y.value = 4;
|
||||
expect(a.x).toBe(3);
|
||||
expect(a.y).toBe(4);
|
||||
|
||||
// reactivity
|
||||
let dummyX, dummyY;
|
||||
watchEffect(() => {
|
||||
dummyX = x.value;
|
||||
dummyY = y.value;
|
||||
});
|
||||
expect(dummyX).toBe(x.value);
|
||||
expect(dummyY).toBe(y.value);
|
||||
|
||||
// mutating source should trigger effect using the proxy refs
|
||||
a.x = 4;
|
||||
a.y = 5;
|
||||
expect(dummyX).toBe(4);
|
||||
expect(dummyY).toBe(5);
|
||||
});
|
||||
|
||||
it('toRefs reactive array', () => {
|
||||
const arr = reactive(['a', 'b', 'c']);
|
||||
const refs = toRefs(arr);
|
||||
|
||||
expect(Array.isArray(refs)).toBe(true);
|
||||
|
||||
refs[0].value = '1';
|
||||
expect(arr[0]).toBe('1');
|
||||
|
||||
arr[1] = '2';
|
||||
expect(refs[1].value).toBe('2');
|
||||
});
|
||||
});
|
|
@ -42,7 +42,7 @@ describe('Dollar store access', () => {
|
|||
function App() {
|
||||
const logStore = useLogStore();
|
||||
|
||||
return <div id={RESULT_ID}>{logStore.$c.length()}</div>;
|
||||
return <div id={RESULT_ID}>{logStore.$c.length}</div>;
|
||||
}
|
||||
|
||||
Inula.render(<App />, container);
|
||||
|
@ -64,7 +64,7 @@ describe('Dollar store access', () => {
|
|||
>
|
||||
add
|
||||
</button>
|
||||
<p id={RESULT_ID}>{logStore.$c.length()}</p>
|
||||
<p id={RESULT_ID}>{logStore.$c.length}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"homepage": "",
|
||||
"bugs": "",
|
||||
"license": "MulanPSL2",
|
||||
"main": "./build/index.js",
|
||||
"main": "./src/index.ts",
|
||||
"repository": {},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
|
|
@ -57,7 +57,7 @@ import {
|
|||
} from './external/InulaIs';
|
||||
import { createStore, useStore, clearStore } from './inulax/store/StoreHandler';
|
||||
import { reactive, useReactive, toRaw } from './inulax/reactive/Reactive';
|
||||
import { ref, useReference, isRef, unref, shallowRef } from './inulax/reactive/Ref';
|
||||
import { ref, useReference, isRef, unref, shallowRef, toRef, toRefs } from './inulax/reactive/Ref';
|
||||
import * as reduxAdapter from './inulax/adapters/redux';
|
||||
import { watch, watchEffect, useWatch } from './inulax/reactive/Watch';
|
||||
import { computed, useComputed } from './inulax/reactive/Computed';
|
||||
|
@ -73,7 +73,7 @@ import {
|
|||
} from './dom/DOMExternal';
|
||||
|
||||
import { syncUpdates as flushSync } from './renderer/TreeBuilder';
|
||||
import { isReactive, isShallow } from './inulax/CommonUtils';
|
||||
import { isReactive, isShallow, isReadonly } from './inulax/CommonUtils';
|
||||
|
||||
const vueReactive = {
|
||||
ref,
|
||||
|
@ -81,10 +81,13 @@ const vueReactive = {
|
|||
isRef,
|
||||
unref,
|
||||
shallowRef,
|
||||
toRef,
|
||||
toRefs,
|
||||
reactive,
|
||||
useReactive,
|
||||
isReactive,
|
||||
isShallow,
|
||||
isReadonly,
|
||||
computed,
|
||||
useComputed,
|
||||
watchEffect,
|
||||
|
@ -208,5 +211,8 @@ export {
|
|||
export * from './types';
|
||||
export * from './inulax/types/ReactiveTypes';
|
||||
export * from './inulax/types/ProxyTypes';
|
||||
export * from './inulax/types/StoreTypes';
|
||||
export * from './inulax/types/StoreTypes';
|
||||
export { ComputedImpl } from './inulax/reactive/Computed';
|
||||
|
||||
export default Inula;
|
||||
|
|
|
@ -218,3 +218,7 @@ export function isShallow(value: unknown): boolean {
|
|||
export function isReactive(value: unknown) {
|
||||
return !!(value && !!value[KeyTypes.RAW_VALUE]);
|
||||
}
|
||||
|
||||
export function isReadonly(value: unknown): boolean {
|
||||
return !!(value && value[ReactiveFlags.IS_READONLY]);
|
||||
}
|
||||
|
|
|
@ -32,4 +32,6 @@ export enum KeyTypes {
|
|||
|
||||
export enum ReactiveFlags {
|
||||
IS_SHALLOW = '_isShallow',
|
||||
IS_READONLY = '_isReadonly',
|
||||
IS_REF = '_isRef',
|
||||
}
|
||||
|
|
|
@ -161,13 +161,6 @@ const c = computed(() => 1, {
|
|||
});
|
||||
```
|
||||
|
||||
5. computed中不支持第二个参数debugOptions。
|
||||
```js
|
||||
const c = computed(() => 1, {
|
||||
onTrack, // 不支持
|
||||
});
|
||||
```
|
||||
|
||||
6. computed.effect.stop 改为 computed.stop。
|
||||
```js
|
||||
it('should no longer update when stopped', () => {
|
||||
|
|
|
@ -34,7 +34,7 @@ export function getObserver(rawObj: any): IObserver {
|
|||
return rawObserverMap.get(rawObj) as IObserver;
|
||||
}
|
||||
|
||||
function setObserverKey(rawObj: any, observer: IObserver): void {
|
||||
function setObserver(rawObj: any, observer: IObserver): void {
|
||||
rawObserverMap.set(rawObj, observer);
|
||||
}
|
||||
|
||||
|
@ -59,7 +59,7 @@ export function createProxy(rawObj: any, listener?: CurrentListener, isDeepProxy
|
|||
let observer = getObserver(rawObj);
|
||||
if (!observer) {
|
||||
observer = (isDeepProxy ? new Observer() : new HooklessObserver()) as IObserver;
|
||||
setObserverKey(rawObj, observer);
|
||||
setObserver(rawObj, observer);
|
||||
}
|
||||
|
||||
deepProxyMap.set(rawObj, isDeepProxy);
|
||||
|
|
|
@ -83,10 +83,6 @@ export function baseGetFun<T extends Record<string | symbol, any> | any[]>(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
if (key === KeyTypes.VALUE) {
|
||||
return receiver;
|
||||
}
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
if (key === KeyTypes.WATCH) {
|
||||
|
|
|
@ -43,6 +43,7 @@ export class ComputedImpl<T = any> {
|
|||
|
||||
private readonly observer: Observer = new Observer();
|
||||
readonly _isRef = true;
|
||||
readonly _isReadonly = true;
|
||||
|
||||
constructor(fn: ComputedFN<T>) {
|
||||
this.fn = fn;
|
||||
|
|
|
@ -13,14 +13,12 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { isObject, isSame, isShallow } from '../CommonUtils';
|
||||
import { reactive, toRaw } from './Reactive';
|
||||
import { isArray, isObject, isSame, isShallow } from '../CommonUtils';
|
||||
import { toRaw } from './Reactive';
|
||||
import { Observer } from '../proxy/Observer';
|
||||
import { KeyTypes, OBSERVER_KEY } from '../Constants';
|
||||
import { MaybeRef, RefType, UnwrapRef } from '../types/ReactiveTypes';
|
||||
import { KeyTypes, OBSERVER_KEY, ReactiveFlags } from '../Constants';
|
||||
import { IfAny, MaybeRef, RefType, ToRef, ToRefs, UnwrapRef } from '../types/ReactiveTypes';
|
||||
import { registerDestroyFunction } from '../store/StoreHandler';
|
||||
import { getProcessingVNode } from '../../renderer/GlobalVar';
|
||||
import { FunctionComponent } from '../../renderer/vnode/VNodeTags';
|
||||
import { useRef } from '../../renderer/hooks/HookExternal';
|
||||
import { createProxy } from '../proxy/ProxyHandler';
|
||||
import { Listener } from '../types/ProxyTypes';
|
||||
|
@ -97,7 +95,7 @@ class RefImpl<T> {
|
|||
|
||||
export function isRef<T>(ref: MaybeRef<T>): ref is RefType<T>;
|
||||
export function isRef(ref: any): ref is RefType {
|
||||
return Boolean(ref && ref._isRef);
|
||||
return Boolean(ref && ref[ReactiveFlags.IS_REF]);
|
||||
}
|
||||
|
||||
export function toReactive<T extends unknown>(value: T): T {
|
||||
|
@ -110,7 +108,6 @@ export function unref<T>(ref: MaybeRef<T>): T {
|
|||
|
||||
declare const ShallowRefMarker: unique symbol;
|
||||
export type ShallowRef<T = any> = RefType<T> & { [ShallowRefMarker]?: true };
|
||||
export type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
|
||||
|
||||
export function shallowRef<T>(
|
||||
value: T
|
||||
|
@ -119,3 +116,73 @@ export function shallowRef<T = any>(): ShallowRef<T | undefined>;
|
|||
export function shallowRef(value?: unknown) {
|
||||
return createRef(value, true);
|
||||
}
|
||||
|
||||
export function toRef<T>(
|
||||
value: T
|
||||
): T extends () => infer R ? Readonly<RefType<R>> : T extends RefType ? T : RefType<UnwrapRef<T>>;
|
||||
export function toRef<T extends Record<string, any>, K extends keyof T>(object: T, key: K): ToRef<T[K]>;
|
||||
export function toRef<T extends Record<string, any>, K extends keyof T>(
|
||||
object: T,
|
||||
key: K,
|
||||
defaultValue: T[K]
|
||||
): ToRef<Exclude<T[K], undefined>>;
|
||||
export function toRef(source: Record<string, any> | MaybeRef, key?: string, defaultValue?: unknown): RefType {
|
||||
if (isRef(source)) {
|
||||
return source;
|
||||
} else if (typeof source === 'function') {
|
||||
return new GetterRefImpl(source) as any;
|
||||
} else if (isObject(source) && arguments.length > 1) {
|
||||
return propertyToRef(source, key!, defaultValue);
|
||||
} else {
|
||||
return ref(source);
|
||||
}
|
||||
}
|
||||
|
||||
class GetterRefImpl<T> {
|
||||
public readonly _isRef = true;
|
||||
public readonly _isReadonly = true;
|
||||
private readonly _getter: () => T;
|
||||
|
||||
constructor(getter: () => T) {
|
||||
this._getter = getter;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._getter();
|
||||
}
|
||||
}
|
||||
|
||||
function propertyToRef(source: Record<string, any>, key: string, defaultValue?: unknown) {
|
||||
const val = source[key];
|
||||
return isRef(val) ? val : (new ObjectRefImpl(source, key, defaultValue) as any);
|
||||
}
|
||||
|
||||
class ObjectRefImpl<T extends Record<string, any>, K extends keyof T> {
|
||||
public readonly _isRef = true;
|
||||
private readonly _object: T;
|
||||
private readonly _key: K;
|
||||
private readonly _defaultValue?: T[K];
|
||||
|
||||
constructor(object: T, key: K, defaultValue?: T[K]) {
|
||||
this._object = object;
|
||||
this._key = key;
|
||||
this._defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
get value() {
|
||||
const val = this._object[this._key];
|
||||
return val === undefined ? this._defaultValue! : val;
|
||||
}
|
||||
|
||||
set value(newVal) {
|
||||
this._object[this._key] = newVal;
|
||||
}
|
||||
}
|
||||
|
||||
export function toRefs<T extends Record<string, any>>(object: T): ToRefs<T> {
|
||||
const ret: any = isArray(object) ? new Array(object.length) : {};
|
||||
for (const key in object) {
|
||||
ret[key] = propertyToRef(object, key);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
|
|
@ -53,14 +53,15 @@ const idGenerator = {
|
|||
};
|
||||
|
||||
const storeMap = new Map<string, StoreObj<any, any, any>>();
|
||||
const pendingMap = new WeakMap<any, boolean | number>();
|
||||
|
||||
// 通过该方法执行store.$queue中的action
|
||||
function tryNextAction(storeObj, proxyObj, config, plannedActions) {
|
||||
if (!plannedActions.length) {
|
||||
if (proxyObj.$pending) {
|
||||
if (pendingMap.get(proxyObj)) {
|
||||
const timestamp = Date.now();
|
||||
const duration = timestamp - proxyObj.$pending;
|
||||
proxyObj.$pending = false;
|
||||
const duration = timestamp - (pendingMap.get(proxyObj) as number);
|
||||
pendingMap.set(proxyObj, false);
|
||||
devtools.emit(QUEUE_FINISHED, {
|
||||
store: storeObj,
|
||||
endedAt: timestamp,
|
||||
|
@ -165,7 +166,7 @@ export function createStore<S extends Record<string, any>, A extends UserActions
|
|||
const proxyObj = createProxy(config.state, listener, !config.options?.isReduxAdapter);
|
||||
|
||||
if (proxyObj !== undefined) {
|
||||
proxyObj.$pending = false;
|
||||
pendingMap.set(proxyObj, false);
|
||||
}
|
||||
|
||||
const $a: Partial<StoreActions<S, A>> = {};
|
||||
|
@ -173,12 +174,13 @@ export function createStore<S extends Record<string, any>, A extends UserActions
|
|||
const $c: Partial<ComputedValues<S, C>> = {};
|
||||
const storeObj = {
|
||||
id,
|
||||
$state: proxyObj,
|
||||
$s: proxyObj,
|
||||
$a: $a as StoreActions<S, A>,
|
||||
$c: $c as ComputedValues<S, C>,
|
||||
$queue: $queue as QueuedStoreActions<S, A>,
|
||||
$config: config,
|
||||
$listeners: [
|
||||
$subscriptions: [
|
||||
change => {
|
||||
devtools.emit(STATE_CHANGE, {
|
||||
store: storeObj,
|
||||
|
@ -188,16 +190,19 @@ export function createStore<S extends Record<string, any>, A extends UserActions
|
|||
],
|
||||
$subscribe: listener => {
|
||||
devtools.emit(SUBSCRIBED, { store: storeObj, listener });
|
||||
storeObj.$listeners.push(listener);
|
||||
storeObj.$subscriptions.push(listener);
|
||||
return () => {
|
||||
storeObj.$unsubscribe(listener);
|
||||
};
|
||||
},
|
||||
$unsubscribe: listener => {
|
||||
devtools.emit(UNSUBSCRIBED, { store: storeObj });
|
||||
storeObj.$listeners = storeObj.$listeners.filter(item => item != listener);
|
||||
storeObj.$subscriptions = storeObj.$subscriptions.filter(item => item != listener);
|
||||
},
|
||||
} as unknown as StoreObj<S, A, C>;
|
||||
|
||||
listener.current = (...args) => {
|
||||
storeObj.$listeners.forEach(listener => listener(...args));
|
||||
storeObj.$subscriptions.forEach(listener => listener(...args));
|
||||
};
|
||||
|
||||
const plannedActions: PlannedAction<S, ActionFunction<S>>[] = [];
|
||||
|
@ -217,11 +222,11 @@ export function createStore<S extends Record<string, any>, A extends UserActions
|
|||
fromQueue: true,
|
||||
});
|
||||
return new Promise(resolve => {
|
||||
if (!proxyObj.$pending) {
|
||||
proxyObj.$pending = Date.now();
|
||||
if (!pendingMap.get(proxyObj)) {
|
||||
pendingMap.set(proxyObj, Date.now());
|
||||
devtools.emit(QUEUE_PENDING, {
|
||||
store: storeObj,
|
||||
startedAt: proxyObj.$pending,
|
||||
startedAt: pendingMap.get(proxyObj),
|
||||
});
|
||||
|
||||
const result = config.actions![action].bind(storeObj, proxyObj)(...payload);
|
||||
|
@ -279,12 +284,15 @@ export function createStore<S extends Record<string, any>, A extends UserActions
|
|||
|
||||
if (config.computed) {
|
||||
Object.keys(config.computed).forEach(computeKey => {
|
||||
// 让store.$c[computeKey]可以访问到computed方法
|
||||
($c as any)[computeKey] = config.computed![computeKey].bind(storeObj, readonlyProxy(proxyObj));
|
||||
const computeFn = config.computed![computeKey].bind(storeObj, readonlyProxy(proxyObj));
|
||||
// 让store.$c[computeKey]可以访问到computed的值
|
||||
Object.defineProperty($c, computeKey, {
|
||||
get: computeFn as () => any,
|
||||
});
|
||||
|
||||
// 让store[computeKey]可以访问到computed的值
|
||||
Object.defineProperty(storeObj, computeKey, {
|
||||
get: $c[computeKey] as () => any,
|
||||
get: computeFn as () => any,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -44,3 +44,10 @@ export type ReactiveRet<T> = T extends FnType | BaseTypes | RefType
|
|||
[P in keyof T]: P extends symbol ? T[P] : UnwrapRef<T[P]>;
|
||||
}
|
||||
: T;
|
||||
|
||||
export type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
|
||||
|
||||
export type ToRef<T> = IfAny<T, RefType<T>, [T] extends [RefType] ? T : RefType<T>>;
|
||||
export type ToRefs<T = any> = {
|
||||
[K in keyof T]: ToRef<T[K]>;
|
||||
};
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { AddWatchProp } from './ProxyTypes';
|
||||
import { AddWatchProp, Listener } from './ProxyTypes';
|
||||
|
||||
export type StoreConfig<
|
||||
S extends Record<string, unknown>,
|
||||
|
@ -48,17 +48,23 @@ type Action<T extends ActionFunction<any>, S extends Record<string, unknown>> =
|
|||
...args: RemoveFirstFromTuple<Parameters<T>>
|
||||
) => ReturnType<T>;
|
||||
|
||||
export type StoreObj<S extends Record<string, unknown>, A extends UserActions<S>, C extends UserComputedValues<S>> = {
|
||||
export type StoreComputed<S extends Record<string, unknown>, C extends UserComputedValues<S>> = {
|
||||
[K in keyof C]: ReturnType<C[K]>;
|
||||
};
|
||||
|
||||
export type StoreObj<
|
||||
S extends Record<string, unknown> = Record<string, unknown>,
|
||||
A extends UserActions<S> = UserActions<S>,
|
||||
C extends UserComputedValues<S> = UserComputedValues<S>,
|
||||
> = {
|
||||
$s: AddWatchProp<S>;
|
||||
$state: AddWatchProp<S>;
|
||||
// $s: S;
|
||||
// $state: S;
|
||||
$a: StoreActions<S, A>;
|
||||
$c: UserComputedValues<S>;
|
||||
$c: StoreComputed<S, C>;
|
||||
$queue: QueuedStoreActions<S, A>;
|
||||
$listeners;
|
||||
$subscribe: (listener: (mutation) => void) => void;
|
||||
$unsubscribe: (listener: (mutation) => void) => void;
|
||||
$subscriptions: Array<Listener>;
|
||||
$subscribe: (listener: Listener) => void;
|
||||
$unsubscribe: (listener: Listener) => void;
|
||||
} & { [K in keyof AddWatchProp<S>]: AddWatchProp<S>[K] } & { [K in keyof A]: Action<A[K], S> } & {
|
||||
[K in keyof C]: ReturnType<C[K]>;
|
||||
};
|
||||
|
@ -76,11 +82,9 @@ type RemoveFirstFromTuple<T extends any[]> = T['length'] extends 0
|
|||
: [];
|
||||
|
||||
export type UserComputedValues<S extends Record<string, unknown>> = {
|
||||
[K: string]: ComputedFunction<S>;
|
||||
[K: string]: (state: S) => any;
|
||||
};
|
||||
|
||||
type ComputedFunction<S extends Record<string, unknown>> = (state: S) => any;
|
||||
|
||||
export type AsyncAction<T extends ActionFunction<any>, S extends Record<string, unknown>> = (
|
||||
this: StoreObj<S, any, any>,
|
||||
...args: RemoveFirstFromTuple<Parameters<T>>
|
||||
|
|
Loading…
Reference in New Issue