feat: add inula-adapter/pinia 和 inula-adapter/vuex

This commit is contained in:
chaoling 2024-05-23 10:03:30 +08:00
parent 0f75e48f79
commit 96c4f0c337
57 changed files with 2952 additions and 697 deletions

View File

@ -0,0 +1,7 @@
## 适配Vue
### 生命周期
### pinia
### vuex

View File

@ -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);
});
```

View File

@ -0,0 +1,10 @@
## 适配Vue
### 生命周期
onBeforeMount
onMounted
onBeforeUpdate
onUpdated
onBeforeUnmount
onUnmounted

View File

@ -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);
},
],
});
```

View File

@ -0,0 +1,5 @@
{
"module": "./esm/pinia-adapter.js",
"main": "./cjs/pinia-adapter.js",
"types": "./@types/index.d.ts"
}

View File

@ -0,0 +1,5 @@
{
"module": "./esm/vuex-adapter.js",
"main": "./cjs/vuex-adapter.js",
"types": "./@types/index.d.ts"
}

View File

@ -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"

View File

@ -1,18 +0,0 @@
接口差异:
1、$patch不需要可以直接赋值
```ts
actions: {
setFoo(foo) {
// not support
this.$patch({ nested: { foo } });
// support
this.nested = { foo };
}
}
```
2、watch写法不同

View File

@ -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"
}
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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);
});
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -1,13 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true
},
"ts-node": {
"esm": true
}
}

View File

@ -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')];

View File

@ -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'),
];

View File

@ -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.

View File

@ -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增加第一个参数storepinia不需要
* @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;
}

View File

@ -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]>>>;
};

View File

@ -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?.();

View File

@ -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;

View File

@ -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';

View File

@ -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;
};

View File

@ -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}`];
},
}
);
}

View File

@ -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() {},

View File

@ -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');
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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 }));
});
});

View File

@ -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', () => {

View File

@ -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);
});
});
});

View File

@ -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);
});
});

View File

@ -0,0 +1,10 @@
{
"files": [
"src/pinia/index.ts"
],
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": true,
"declarationDir": "./build/pinia/@types"
},
}

View File

@ -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"
},
}

View File

@ -0,0 +1,10 @@
{
"files": [
"src/vuex/index.ts"
],
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": true,
"declarationDir": "./build/vuex/@types"
},
}

View File

@ -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',

View File

@ -1,2 +0,0 @@
## 适配Vue

View File

@ -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');
});
});

View File

@ -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>
);
}

View File

@ -8,7 +8,7 @@
"homepage": "",
"bugs": "",
"license": "MulanPSL2",
"main": "./build/index.js",
"main": "./src/index.ts",
"repository": {},
"engines": {
"node": ">=0.10.0"

View File

@ -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;

View File

@ -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]);
}

View File

@ -32,4 +32,6 @@ export enum KeyTypes {
export enum ReactiveFlags {
IS_SHALLOW = '_isShallow',
IS_READONLY = '_isReadonly',
IS_REF = '_isRef',
}

View File

@ -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', () => {

View File

@ -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);

View File

@ -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) {

View File

@ -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;

View File

@ -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;
}

View File

@ -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,
});
});
}

View File

@ -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]>;
};

View File

@ -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>>