Compare commits
5 Commits
master
...
vue-adapte
Author | SHA1 | Date |
---|---|---|
|
96c4f0c337 | |
|
0f75e48f79 | |
|
b725b0d98c | |
|
d7101ff5e3 | |
|
05f0610c99 |
|
@ -8,4 +8,3 @@ pnpm-lock.yaml
|
|||
build
|
||||
/packages/inula-router/connectRouter
|
||||
/packages/inula-router/router
|
||||
.inula-max
|
||||
|
|
85
package.json
85
package.json
|
@ -9,9 +9,6 @@
|
|||
"prettier": "prettier .prettierrc.js -w packages/**/*.{ts,tsx,js,jsx}",
|
||||
"build:inula": "pnpm -F openinula build",
|
||||
"test:inula": "pnpm -F openinula test",
|
||||
"test:inula-intl": "pnpm -F inula-intl test",
|
||||
"test:inula-request": "pnpm -F inula-request test",
|
||||
"test:inula-router": "pnpm -F inula-router test",
|
||||
"build:inula-cli": "pnpm -F inula-cli build",
|
||||
"build:inula-intl": "pnpm -F inula-intl build",
|
||||
"build:inula-request": "pnpm -F inula-request build",
|
||||
|
@ -25,48 +22,46 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.23.7",
|
||||
"@babel/plugin-proposal-class-properties": "7.18.6",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.21.0",
|
||||
"@babel/plugin-proposal-private-methods": "7.18.6",
|
||||
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
|
||||
"@babel/plugin-syntax-jsx": "7.23.3",
|
||||
"@babel/plugin-transform-arrow-functions": "7.23.3",
|
||||
"@babel/plugin-transform-block-scoped-functions": "7.23.3",
|
||||
"@babel/plugin-transform-block-scoping": "7.23.4",
|
||||
"@babel/plugin-transform-classes": "7.23.8",
|
||||
"@babel/plugin-transform-computed-properties": "7.23.3",
|
||||
"@babel/plugin-transform-destructuring": "7.23.3",
|
||||
"@babel/plugin-transform-for-of": "7.23.6",
|
||||
"@babel/plugin-transform-literals": "7.23.3",
|
||||
"@babel/plugin-transform-object-assign": "7.23.3",
|
||||
"@babel/plugin-transform-object-super": "7.23.3",
|
||||
"@babel/plugin-transform-parameters": "7.23.3",
|
||||
"@babel/plugin-transform-react-jsx": "7.23.4",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.23.3",
|
||||
"@babel/plugin-transform-runtime": "7.23.7",
|
||||
"@babel/plugin-transform-shorthand-properties": "7.23.3",
|
||||
"@babel/plugin-transform-spread": "7.23.3",
|
||||
"@babel/plugin-transform-template-literals": "7.23.3",
|
||||
"@babel/preset-env": "7.23.8",
|
||||
"@babel/preset-typescript": "7.23.3",
|
||||
"@babel/runtime": "7.23.8",
|
||||
"@commitlint/cli": "^17.8.1",
|
||||
"@commitlint/config-conventional": "^17.8.1",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@babel/core": "7.16.7",
|
||||
"@babel/plugin-proposal-class-properties": "7.16.7",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.16.7",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.16.7",
|
||||
"@babel/plugin-proposal-private-methods": "7.16.7",
|
||||
"@babel/plugin-proposal-private-property-in-object": "7.16.7",
|
||||
"@babel/plugin-syntax-jsx": "7.16.7",
|
||||
"@babel/plugin-transform-arrow-functions": "7.16.7",
|
||||
"@babel/plugin-transform-block-scoped-functions": "7.16.7",
|
||||
"@babel/plugin-transform-block-scoping": "7.16.7",
|
||||
"@babel/plugin-transform-classes": "7.16.7",
|
||||
"@babel/plugin-transform-computed-properties": "7.16.7",
|
||||
"@babel/plugin-transform-destructuring": "7.16.7",
|
||||
"@babel/plugin-transform-for-of": "7.16.7",
|
||||
"@babel/plugin-transform-literals": "7.16.7",
|
||||
"@babel/plugin-transform-object-assign": "7.16.7",
|
||||
"@babel/plugin-transform-object-super": "7.16.7",
|
||||
"@babel/plugin-transform-parameters": "7.16.7",
|
||||
"@babel/plugin-transform-react-jsx": "7.16.7",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.16.7",
|
||||
"@babel/plugin-transform-runtime": "7.16.7",
|
||||
"@babel/plugin-transform-shorthand-properties": "7.16.7",
|
||||
"@babel/plugin-transform-spread": "7.16.7",
|
||||
"@babel/plugin-transform-template-literals": "7.16.7",
|
||||
"@babel/preset-env": "7.16.7",
|
||||
"@babel/preset-typescript": "7.16.7",
|
||||
"@babel/runtime": "7.16.7",
|
||||
"@commitlint/cli": "^18.4.4",
|
||||
"@commitlint/config-conventional": "^18.4.4",
|
||||
"@rollup/plugin-babel": "^5.3.1",
|
||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||
"@rollup/plugin-replace": "^4.0.0",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/node": "^17.0.18",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"@babel/parser": "^7.24.7",
|
||||
"magic-string": "^0.30.10",
|
||||
"babel-jest": "^29.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "4.8.0",
|
||||
"@typescript-eslint/parser": "4.8.0",
|
||||
"babel-jest": "^27.5.1",
|
||||
"ejs": "^3.1.8",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint": "7.13.0",
|
||||
"eslint-config-prettier": "^6.9.0",
|
||||
"eslint-plugin-jest": "^22.15.0",
|
||||
"eslint-plugin-no-function-declare-after-return": "^1.0.0",
|
||||
|
@ -77,13 +72,9 @@
|
|||
"lint-staged": "^15.2.0",
|
||||
"openinula": "workspace:*",
|
||||
"prettier": "^3.1.1",
|
||||
"rollup": "^2.79.1",
|
||||
"rollup-plugin-dts": "^6.1.0",
|
||||
"rollup": "^2.75.5",
|
||||
"rollup-plugin-execute": "^1.1.1",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-esbuild": "^6.1.1",
|
||||
"rollup-plugin-polyfill-node": "^0.13.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"engines": {
|
||||
|
|
|
@ -32,8 +32,8 @@ const generatorType = fs
|
|||
});
|
||||
|
||||
const runGenerator = async (templatePath, { name = '', cwd = process.cwd(), args = {} }) => {
|
||||
let currentPath;
|
||||
return new Promise(resolve => {
|
||||
let currentPath;
|
||||
if (name) {
|
||||
mkdirp.sync(name);
|
||||
currentPath = path.join(cwd, name);
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
printWidth: 120, // 一行120字符数,如果超过会进行换行
|
||||
tabWidth: 2, // tab等2个空格
|
||||
useTabs: false, // 用空格缩进行
|
||||
semi: true, // 行尾使用分号
|
||||
singleQuote: true, // 字符串使用单引号
|
||||
quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号
|
||||
jsxSingleQuote: false, // 在JSX中使用双引号
|
||||
trailingComma: 'es5', // 使用尾逗号(对象、数组等)
|
||||
bracketSpacing: true, // 对象的括号间增加空格
|
||||
bracketSameLine: false, // 将多行JSX元素的>放在最后一行的末尾
|
||||
arrowParens: 'avoid', // 在唯一的arrow函数参数周围省略括号
|
||||
vueIndentScriptAndStyle: false, // 不缩进Vue文件中的<script>和<style>标记内的代码
|
||||
endOfLine: 'lf', // 仅限换行(\n)
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
## 适配Vue
|
||||
|
||||
### 生命周期
|
||||
|
||||
### pinia
|
||||
|
||||
### vuex
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
presets: ['@babel/preset-typescript', ['@babel/preset-env', { targets: { node: 'current' } }]],
|
||||
plugins: [
|
||||
'@babel/plugin-syntax-jsx',
|
||||
[
|
||||
'@babel/plugin-transform-react-jsx',
|
||||
{
|
||||
runtime: 'automatic',
|
||||
importSource: 'openinula',
|
||||
},
|
||||
],
|
||||
['@babel/plugin-proposal-class-properties', { loose: true }],
|
||||
['@babel/plugin-proposal-private-methods', { loose: true }],
|
||||
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
|
||||
'@babel/plugin-transform-object-assign',
|
||||
'@babel/plugin-transform-object-super',
|
||||
['@babel/plugin-proposal-object-rest-spread', { loose: true, useBuiltIns: true }],
|
||||
['@babel/plugin-transform-template-literals', { loose: true }],
|
||||
'@babel/plugin-transform-arrow-functions',
|
||||
'@babel/plugin-transform-literals',
|
||||
'@babel/plugin-transform-for-of',
|
||||
'@babel/plugin-transform-block-scoped-functions',
|
||||
'@babel/plugin-transform-classes',
|
||||
'@babel/plugin-transform-shorthand-properties',
|
||||
'@babel/plugin-transform-computed-properties',
|
||||
'@babel/plugin-transform-parameters',
|
||||
['@babel/plugin-transform-spread', { loose: true, useBuiltIns: true }],
|
||||
['@babel/plugin-transform-block-scoping', { throwIfClosureRequired: false }],
|
||||
['@babel/plugin-transform-destructuring', { loose: true, useBuiltIns: true }],
|
||||
'@babel/plugin-transform-runtime',
|
||||
'@babel/plugin-proposal-nullish-coalescing-operator',
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
],
|
||||
};
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"name": "@inula/vue-adapter",
|
||||
"version": "0.0.1",
|
||||
"description": "vue adapter",
|
||||
"main": "./vue/cjs/vue-adapter.js",
|
||||
"module": "./vue/esm/vue-adapter.js",
|
||||
"types": "./vue/@types/index.d.ts",
|
||||
"files": [
|
||||
"build/**/*",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest --ui",
|
||||
"build": "rollup -c ./scripts/rollup.config.js && npm run build-types",
|
||||
"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:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.21.3",
|
||||
"@babel/plugin-proposal-class-properties": "7.16.7",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.16.7",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.16.7",
|
||||
"@babel/plugin-syntax-jsx": "7.16.7",
|
||||
"@babel/plugin-transform-arrow-functions": "7.16.7",
|
||||
"@babel/plugin-transform-block-scoped-functions": "7.16.7",
|
||||
"@babel/plugin-transform-block-scoping": "7.16.7",
|
||||
"@babel/plugin-transform-classes": "7.16.7",
|
||||
"@babel/plugin-transform-computed-properties": "7.16.7",
|
||||
"@babel/plugin-transform-destructuring": "7.16.7",
|
||||
"@babel/plugin-transform-for-of": "7.16.7",
|
||||
"@babel/plugin-transform-literals": "7.16.7",
|
||||
"@babel/plugin-transform-object-assign": "7.16.7",
|
||||
"@babel/plugin-transform-object-super": "7.16.7",
|
||||
"@babel/plugin-transform-parameters": "7.16.7",
|
||||
"@babel/plugin-transform-react-jsx": "7.16.7",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.16.7",
|
||||
"@babel/plugin-transform-runtime": "7.16.7",
|
||||
"@babel/plugin-transform-shorthand-properties": "7.16.7",
|
||||
"@babel/plugin-transform-spread": "7.16.7",
|
||||
"@babel/plugin-transform-template-literals": "7.16.7",
|
||||
"@babel/preset-env": "7.16.7",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@rollup/plugin-babel": "^6.0.3",
|
||||
"@rollup/plugin-node-resolve": "^15.1.0",
|
||||
"prettier": "2.8.8",
|
||||
"rollup": "2.79.1",
|
||||
"rollup-plugin-dts": "^6.0.1",
|
||||
"rollup-plugin-terser": "^5.1.3",
|
||||
"typescript": "4.9.3",
|
||||
"@vitest/ui": "^0.34.5",
|
||||
"jsdom": "^24.0.0",
|
||||
"vitest": "^0.34.5",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@testing-library/user-event": "^12.1.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openinula": ">=0.1.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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 fs from 'fs';
|
||||
import path from 'path';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
|
||||
function deleteFolder(filePath) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
if (fs.lstatSync(filePath).isDirectory()) {
|
||||
const files = fs.readdirSync(filePath);
|
||||
files.forEach(file => {
|
||||
const nextFilePath = path.join(filePath, file);
|
||||
const states = fs.lstatSync(nextFilePath);
|
||||
if (states.isDirectory()) {
|
||||
deleteFolder(nextFilePath);
|
||||
} else {
|
||||
fs.unlinkSync(nextFilePath);
|
||||
}
|
||||
});
|
||||
fs.rmdirSync(filePath);
|
||||
} else if (fs.lstatSync(filePath).isFile()) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除非空文件夹
|
||||
* @param folders {string[]}
|
||||
* @returns {{buildEnd(): void, name: string}}
|
||||
*/
|
||||
export function cleanUp(folders) {
|
||||
return {
|
||||
name: 'clean-up',
|
||||
buildEnd() {
|
||||
folders.forEach(f => deleteFolder(f));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildTypeConfig(name) {
|
||||
return {
|
||||
input: [`./build/${name}/@types/${name}/index.d.ts`],
|
||||
output: {
|
||||
file: `./build/${name}/@types/index.d.ts`,
|
||||
},
|
||||
plugins: [dts(), cleanUp([`./build/${name}/@types/`])],
|
||||
};
|
||||
}
|
||||
|
||||
export default [buildTypeConfig('vue'), buildTypeConfig('pinia'), buildTypeConfig('vuex')];
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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 path from 'path';
|
||||
import fs from 'fs';
|
||||
import babel from '@rollup/plugin-babel';
|
||||
import nodeResolve from '@rollup/plugin-node-resolve';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
const outDir = path.join(rootDir, 'build');
|
||||
|
||||
const extensions = ['.js', '.ts', '.tsx'];
|
||||
|
||||
if (!fs.existsSync(outDir)) {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
}
|
||||
|
||||
const getConfig = (mode, name) => {
|
||||
const prod = mode.startsWith('prod');
|
||||
const outputList = [
|
||||
{
|
||||
file: path.join(outDir, `${name}/cjs/${name}-adapter.${prod ? 'min.' : ''}js`),
|
||||
sourcemap: 'true',
|
||||
format: 'cjs',
|
||||
},
|
||||
{
|
||||
file: path.join(outDir, `${name}/umd/${name}-adapter.${prod ? 'min.' : ''}js`),
|
||||
name: 'VueAdapter',
|
||||
sourcemap: 'true',
|
||||
format: 'umd',
|
||||
},
|
||||
];
|
||||
if (!prod) {
|
||||
outputList.push({
|
||||
file: path.join(outDir, `${name}/esm/${name}-adapter.js`),
|
||||
sourcemap: 'true',
|
||||
format: 'esm',
|
||||
});
|
||||
}
|
||||
return {
|
||||
input: path.join(rootDir, `/src/${name}/index.ts`),
|
||||
output: outputList,
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
extensions,
|
||||
modulesOnly: true,
|
||||
}),
|
||||
babel({
|
||||
exclude: 'node_modules/**',
|
||||
configFile: path.join(rootDir, '/babel.config.js'),
|
||||
babelHelpers: 'runtime',
|
||||
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`),
|
||||
},
|
||||
]),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
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) 2023 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,11 +13,4 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
export function watch(stateVariable: any, listener: (state: any) => void) {
|
||||
listener = listener.bind(null, stateVariable);
|
||||
stateVariable.addListener(listener);
|
||||
|
||||
return () => {
|
||||
stateVariable.removeListener(listener);
|
||||
};
|
||||
}
|
||||
export * from './pinia';
|
|
@ -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]>>>;
|
||||
};
|
|
@ -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 './lifecycle';
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 { useEffect, useLayoutEffect, useRef } from 'openinula';
|
||||
import { FN } from './types';
|
||||
|
||||
// 用于存储组件是否已挂载的状态
|
||||
const useIsMounted = () => {
|
||||
const isMounted = useRef(false);
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
return isMounted.current;
|
||||
};
|
||||
|
||||
export const onBeforeMount = (fn: FN) => {
|
||||
const isMounted = useIsMounted();
|
||||
if (!isMounted) {
|
||||
fn?.();
|
||||
}
|
||||
};
|
||||
|
||||
export function onMounted(fn: FN) {
|
||||
useEffect(() => {
|
||||
fn?.();
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function onBeforeUpdated(fn: FN) {
|
||||
useEffect(() => {
|
||||
fn?.();
|
||||
});
|
||||
}
|
||||
|
||||
export function onUpdated(fn: FN) {
|
||||
useEffect(() => {
|
||||
fn?.();
|
||||
});
|
||||
}
|
||||
|
||||
export const onBeforeUnmount = (fn: FN) => {
|
||||
useLayoutEffect(() => {
|
||||
return () => {
|
||||
fn?.();
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export function onUnmounted(fn: FN) {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
fn?.();
|
||||
};
|
||||
}, []);
|
||||
}
|
|
@ -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 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}`];
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* 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, vi, expect, beforeEach } from 'vitest';
|
||||
import { defineStore } from '../../src/pinia/pinia';
|
||||
|
||||
let id = 0;
|
||||
function createStore() {
|
||||
return defineStore({
|
||||
id: String(id++),
|
||||
state: () => ({
|
||||
a: true,
|
||||
nested: {
|
||||
foo: 'foo',
|
||||
a: { b: 'string' },
|
||||
},
|
||||
}),
|
||||
getters: {
|
||||
nonA(): boolean {
|
||||
return !this.a;
|
||||
},
|
||||
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', () => {
|
||||
const store = useStore();
|
||||
expect(store.$state.a).toBe(true);
|
||||
store.toggle();
|
||||
expect(store.$state.a).toBe(false);
|
||||
});
|
||||
|
||||
it('store is forced as the context', () => {
|
||||
const store = useStore();
|
||||
expect(store.$state.a).toBe(true);
|
||||
expect(() => {
|
||||
store.toggle.call(null);
|
||||
}).not.toThrow();
|
||||
expect(store.$state.a).toBe(false);
|
||||
});
|
||||
|
||||
it('can call other actions', () => {
|
||||
const store = useStore();
|
||||
expect(store.$state.a).toBe(true);
|
||||
expect(store.$state.nested.foo).toBe('foo');
|
||||
store.combined();
|
||||
expect(store.$state.a).toBe(false);
|
||||
expect(store.$state.nested.foo).toBe('bar');
|
||||
});
|
||||
|
||||
it('throws errors', () => {
|
||||
const store = useStore();
|
||||
expect(() => store.throws()).toThrowError('fail');
|
||||
});
|
||||
|
||||
it('throws async errors', async () => {
|
||||
const store = useStore();
|
||||
expect.assertions(1);
|
||||
await expect(store.rejects()).rejects.toBe('fail');
|
||||
});
|
||||
|
||||
it('can catch async errors', async () => {
|
||||
const store = useStore();
|
||||
expect.assertions(3);
|
||||
const spy = vi.fn();
|
||||
await expect(store.rejects().catch(spy)).resolves.toBe(undefined);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenCalledWith('fail');
|
||||
});
|
||||
|
||||
it('can destructure actions', () => {
|
||||
const store = useStore();
|
||||
const { simple } = store;
|
||||
expect(simple()).toBe('simple');
|
||||
// works with the wrong this
|
||||
expect({ simple }.simple()).toBe('simple');
|
||||
// special this check
|
||||
expect({ $id: 'o', simple }.simple()).toBe('simple');
|
||||
// override the function like devtools do
|
||||
expect(
|
||||
{
|
||||
simple,
|
||||
// otherwise it would fail
|
||||
toggle() {},
|
||||
}.simple()
|
||||
).toBe('simple');
|
||||
});
|
||||
});
|
|
@ -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 }));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
* 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 { render, act, useState } from 'openinula';
|
||||
import { onBeforeUnmount, onUnmounted, onMounted, onBeforeMount, onUpdated } from '../../src/vue/lifecycle';
|
||||
|
||||
describe('lifecycle', () => {
|
||||
it('should call the onBeforeMount', () => {
|
||||
const fn = vi.fn(() => {
|
||||
expect(document.querySelector('span')).toBeNull();
|
||||
});
|
||||
|
||||
const Comp = () => {
|
||||
const [toggle, setToggle] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
{toggle ? <Child /> : null}
|
||||
<button onClick={() => setToggle(false)}>Unmount</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Child = () => {
|
||||
onBeforeMount(fn);
|
||||
return <span />;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
render(<Comp />, container);
|
||||
|
||||
expect(document.querySelector('span')).not.toBeNull();
|
||||
|
||||
act(() => {
|
||||
container.querySelector('button').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call the onMounted', () => {
|
||||
const fn = vi.fn(() => {
|
||||
// 断言在组件卸载之后,子组件不存在于 DOM 中
|
||||
expect(document.querySelector('span')).not.toBeNull();
|
||||
});
|
||||
|
||||
const Comp = () => {
|
||||
const [toggle, setToggle] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
{toggle ? <Child /> : null}
|
||||
<button onClick={() => setToggle(false)}>Unmount</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Child = () => {
|
||||
onMounted(fn);
|
||||
return <span />;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
render(<Comp />, container);
|
||||
|
||||
expect(document.querySelector('span')).not.toBeNull();
|
||||
|
||||
act(() => {
|
||||
container.querySelector('button').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call the onUnmounted after the component unmounts', () => {
|
||||
const fn = vi.fn(() => {
|
||||
// 断言在组件卸载之后,子组件不存在于 DOM 中
|
||||
expect(document.querySelector('span')).not.toBeNull();
|
||||
});
|
||||
|
||||
const Comp = () => {
|
||||
const [toggle, setToggle] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
{toggle ? <Child /> : null}
|
||||
<button onClick={() => setToggle(false)}>Unmount</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Child = () => {
|
||||
onUnmounted(fn);
|
||||
return <span />;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
render(<Comp />, container);
|
||||
|
||||
expect(document.querySelector('span')).not.toBeNull();
|
||||
|
||||
act(() => {
|
||||
container.querySelector('button').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call the onBeforeUnmount before the component unmounts', () => {
|
||||
const fn = vi.fn(() => {
|
||||
// 断言在组件卸载之前,子组件仍然存在于 DOM 中
|
||||
expect(document.querySelector('span')).not.toBeNull();
|
||||
});
|
||||
|
||||
const Comp = () => {
|
||||
const [toggle, setToggle] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
{toggle ? <Child /> : null}
|
||||
<button onClick={() => setToggle(false)}>Unmount</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Child = () => {
|
||||
onBeforeUnmount(fn);
|
||||
return <span />;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
render(<Comp />, container);
|
||||
|
||||
expect(document.querySelector('span')).not.toBeNull();
|
||||
|
||||
act(() => {
|
||||
container.querySelector('button').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(document.querySelector('span')).toBeNull();
|
||||
});
|
||||
|
||||
it('should call the onUpdated/onBeforeUpdated', () => {
|
||||
const fn = vi.fn(() => {
|
||||
expect(document.querySelector('span').outerHTML).toBe('<span>0</span>');
|
||||
});
|
||||
|
||||
const Comp = () => {
|
||||
const [toggle, setToggle] = useState(true);
|
||||
|
||||
onUpdated(fn);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>{toggle ? 1 : 0}</span>
|
||||
<button onClick={() => setToggle(false)}>Unmount</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
render(<Comp />, container);
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(0);
|
||||
expect(document.querySelector('span').outerHTML).toBe('<span>1</span>');
|
||||
|
||||
container.querySelector('button').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -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,35 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./build",
|
||||
"incremental": false,
|
||||
"sourceMap": true,
|
||||
"allowJs": true, // allowJs=true => tsc compile js as module, no type check
|
||||
"checkJs": false, // Disable ts error checking in js
|
||||
"strict": true, // js-ts mixed setting
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": false, // 等大部分js代码改成ts之后再启用.
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitAny": false,
|
||||
"noImplicitThis": true,
|
||||
"module": "CommonJS",
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"target": "es5",
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"allowUnreachableCode": true,
|
||||
"alwaysStrict": true,
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"experimentalDecorators": true,
|
||||
"downlevelIteration": true,
|
||||
"types": ["jest"], // 赋值为空数组使@types/node不会起作用
|
||||
"lib": ["dom", "esnext", "ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020"],
|
||||
"baseUrl": ".",
|
||||
"rootDir": "./src",
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"files": [
|
||||
"src/pinia/index.ts"
|
||||
],
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": true,
|
||||
"declarationDir": "./build/pinia/@types"
|
||||
},
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"files": [
|
||||
"src/vue/index.ts"
|
||||
],
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": true,
|
||||
"declarationDir": "./build/vue/@types"
|
||||
},
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"files": [
|
||||
"src/vuex/index.ts"
|
||||
],
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": true,
|
||||
"declarationDir": "./build/vuex/@types"
|
||||
},
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 react from '@vitejs/plugin-react';
|
||||
|
||||
const alias = {
|
||||
react: 'openinula', // 新增
|
||||
'react-dom': 'openinula', // 新增
|
||||
'react/jsx-dev-runtime': 'openinula/jsx-dev-runtime',
|
||||
};
|
||||
|
||||
export default {
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
},
|
||||
resolve: {
|
||||
alias,
|
||||
},
|
||||
};
|
|
@ -14,3 +14,4 @@
|
|||
*/
|
||||
|
||||
declare module 'crequire';
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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 webpack from 'webpack';
|
||||
import { build } from 'vite';
|
||||
|
||||
export default (api: any) => {
|
||||
api.registerCommand({
|
||||
name: 'build',
|
||||
description: 'build application for production',
|
||||
initialState: api.buildConfig,
|
||||
fn: async function (args: any, state: any) {
|
||||
switch (api.compileMode) {
|
||||
case 'webpack':
|
||||
if (state) {
|
||||
api.applyHook({ name: 'beforeCompile', args: state });
|
||||
state.forEach((s: any) => {
|
||||
webpack(s.config, (err: any, stats: any) => {
|
||||
if (err || stats.hasErrors()) {
|
||||
api.logger.error(`Build failed.err: ${err}, stats:${stats}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
api.logger.error(`Build failed. Can't find build config.`);
|
||||
}
|
||||
break;
|
||||
case 'vite':
|
||||
if (state) {
|
||||
api.applyHook({ name: 'beforeCompile' });
|
||||
build(state);
|
||||
} else {
|
||||
api.logger.error(`Build failed. Can't find build config.`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,42 +0,0 @@
|
|||
{
|
||||
"presets": [
|
||||
["@babel/preset-env", {
|
||||
"targets": {
|
||||
"node": "current"
|
||||
}
|
||||
}],
|
||||
"@babel/preset-typescript",
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-syntax-jsx",
|
||||
[
|
||||
"@babel/plugin-transform-react-jsx",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"importSource": "openinula"
|
||||
}
|
||||
],
|
||||
["@babel/plugin-proposal-class-properties", { "loose": true }],
|
||||
["@babel/plugin-proposal-private-methods", { "loose": true }],
|
||||
["@babel/plugin-proposal-private-property-in-object", { "loose": true }],
|
||||
"@babel/plugin-transform-object-assign",
|
||||
"@babel/plugin-transform-object-super",
|
||||
["@babel/plugin-proposal-object-rest-spread", { "loose": true, "useBuiltIns": true }],
|
||||
["@babel/plugin-transform-template-literals", { "loose": true }],
|
||||
"@babel/plugin-transform-arrow-functions",
|
||||
"@babel/plugin-transform-literals",
|
||||
"@babel/plugin-transform-for-of",
|
||||
"@babel/plugin-transform-block-scoped-functions",
|
||||
"@babel/plugin-transform-classes",
|
||||
"@babel/plugin-transform-shorthand-properties",
|
||||
"@babel/plugin-transform-computed-properties",
|
||||
"@babel/plugin-transform-parameters",
|
||||
["@babel/plugin-transform-spread", { "loose": true, "useBuiltIns": true }],
|
||||
["@babel/plugin-transform-block-scoping", { "throwIfClosureRequired": false }],
|
||||
["@babel/plugin-transform-destructuring", { "loose": true, "useBuiltIns": true }],
|
||||
"@babel/plugin-transform-runtime",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
"@babel/plugin-proposal-optional-chaining"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.
|
||||
*/
|
||||
|
||||
const { preset } = require('./jest.config');
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{ targets: { node: 'current' } },
|
||||
],
|
||||
['@babel/preset-typescript'],
|
||||
[
|
||||
'@babel/preset-react',
|
||||
{
|
||||
runtime: 'automatic',
|
||||
importSource: 'openinula',
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
|
@ -7,12 +7,12 @@ function deleteFolder(filePath) {
|
|||
if (fs.lstatSync(filePath).isDirectory()) {
|
||||
const files = fs.readdirSync(filePath);
|
||||
files.forEach(file => {
|
||||
const nextFilePath = path.join(filePath, file);
|
||||
const states = fs.lstatSync(nextFilePath);
|
||||
const nectFilePath = path.join(filePath, file);
|
||||
const states = fs.lstatSync(nectFilePath);
|
||||
if (states.isDirectory()) {
|
||||
deleteFolder(nextFilePath);
|
||||
deleteFolder(nectFilePath);
|
||||
} else {
|
||||
fs.unlinkSync(nextFilePath);
|
||||
fs.unlinkSync(nectFilePath);
|
||||
}
|
||||
});
|
||||
fs.rmdirSync(filePath);
|
||||
|
@ -31,12 +31,12 @@ export function cleanUp(folders) {
|
|||
return {
|
||||
name: 'clean-up',
|
||||
buildEnd() {
|
||||
folders.forEach(f => deleteFolder(f));
|
||||
folders.forEach(folder => deleteFolder(folder));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildTypeConfig() {
|
||||
function builderTypeConfig() {
|
||||
return {
|
||||
input: './build/@types/index.d.ts',
|
||||
output: {
|
||||
|
@ -47,4 +47,4 @@ function buildTypeConfig() {
|
|||
};
|
||||
}
|
||||
|
||||
export default [buildTypeConfig()];
|
||||
export default [builderTypeConfig()];
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { useState } from 'openinula';
|
||||
import Inula, { useState } from 'openinula';
|
||||
import { IntlProvider } from '../index';
|
||||
import zh from './locale/zh';
|
||||
import en from './locale/en';
|
||||
|
@ -32,29 +32,23 @@ const App = () => {
|
|||
const message = locale === 'zh' ? zh : en;
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntlProvider locale={locale} messages={locale === 'zh' ? zh : en}>
|
||||
<header>Inula-Intl API Test Demo</header>
|
||||
<div className="container">
|
||||
<Example1 />
|
||||
<Example2 />
|
||||
<Example3 locale={locale} setLocale={setLocale} />
|
||||
</div>
|
||||
<div className="container">
|
||||
{/*<Example4 locale={locale} messages={message} />*/}
|
||||
<Example5 />
|
||||
</div>
|
||||
<div className="button">
|
||||
<button onClick={handleChange}>切换语言</button>
|
||||
</div>
|
||||
</IntlProvider>
|
||||
<IntlProvider locale={locale} messages={locale === 'zh' ? zh : en}>
|
||||
<header>Inula-Intl API Test Demo</header>
|
||||
|
||||
<div className="container">
|
||||
<Example1 />
|
||||
<Example2 />
|
||||
<Example3 locale={locale} setLocale={setLocale} />
|
||||
</div>
|
||||
<div className="container">
|
||||
<Example4 locale={locale} messages={message} />
|
||||
</div>
|
||||
<div className="container">
|
||||
<Example5 />
|
||||
<Example6 locale={{ locale }} messages={message} />
|
||||
</div>
|
||||
</>
|
||||
<div className="button">
|
||||
<button onClick={handleChange}>切换语言</button>
|
||||
</div>
|
||||
</IntlProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -13,16 +13,16 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import Inula from 'openinula';
|
||||
import { useIntl } from '../../index';
|
||||
|
||||
const Example1 = () => {
|
||||
const i18n = useIntl();
|
||||
const { i18n } = useIntl();
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<h2>useIntl方式测试Demo</h2>
|
||||
<pre>{i18n.formatMessage({ id: 'text1' })}</pre>
|
||||
<pre>{i18n.$t({ id: 'text1' })}</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
import Inula from 'openinula';
|
||||
import { FormattedMessage } from '../../index';
|
||||
|
||||
const Example2 = () => {
|
||||
|
@ -21,9 +22,6 @@ const Example2 = () => {
|
|||
<pre>
|
||||
<FormattedMessage id="text2" />
|
||||
</pre>
|
||||
<pre>
|
||||
<FormattedMessage id="text5" values={{ testComponent1: <b>123</b>, testComponent2: <b>456</b> }} />
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import Inula from 'openinula';
|
||||
import { FormattedMessage } from '../../index';
|
||||
|
||||
const Example3 = props => {
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import Inula from 'openinula';
|
||||
import { createIntl } from '../../index';
|
||||
|
||||
const Example4 = props => {
|
||||
|
|
|
@ -13,16 +13,23 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import Inula, { Component } from 'openinula';
|
||||
import { injectIntl } from '../../index';
|
||||
|
||||
const Example5 = ({ intl }) => {
|
||||
// 使用intl.formatMessage来获取国际化消息
|
||||
console.log(intl + '------------intl-------------');
|
||||
return (
|
||||
<div className="card">
|
||||
<h2>injectIntl方式测试Demo</h2>
|
||||
<pre>{intl.formatMessage({ id: 'text4' })}</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
class Example5 extends Component<any, any, any> {
|
||||
public constructor(props: any, context) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl } = this.props as any;
|
||||
return (
|
||||
<div className="card">
|
||||
<h2>injectIntl方式测试Demo</h2>
|
||||
<pre>{intl.formatMessage({ id: 'text4' })}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(Example5);
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import Inula from 'openinula';
|
||||
import { createIntl, createIntlCache, RawIntlProvider } from '../../index';
|
||||
import Example6Child from './Example6Child';
|
||||
|
||||
|
@ -20,7 +21,7 @@ const Example6 = (props: any) => {
|
|||
const { locale, messages } = props;
|
||||
|
||||
const cache = createIntlCache();
|
||||
const i18n = createIntl({ locale: locale, messages: messages }, cache);
|
||||
let i18n = createIntl({ locale: locale, messages: messages }, cache);
|
||||
|
||||
return (
|
||||
<RawIntlProvider value={i18n}>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
import { useIntl } from '../../index';
|
||||
|
||||
const Example6Child = () => {
|
||||
const Example6Child = (props: any) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
import Inula from 'openinula';
|
||||
import * as Inula from 'openinula';
|
||||
import App from './App';
|
||||
|
||||
function render() {
|
||||
|
|
|
@ -19,5 +19,4 @@ export default {
|
|||
text2: 'Welcome to the Inula-Intl component!',
|
||||
text3: 'Welcome to the Inula-Intl component!',
|
||||
text4: 'Welcome to the Inula-Intl component!',
|
||||
text5: 'Render a component {testComponent1} {testComponent2}!',
|
||||
};
|
||||
|
|
|
@ -18,5 +18,4 @@ export default {
|
|||
text2: '欢迎使用国际化组件!',
|
||||
text3: '欢迎使用国际化组件!',
|
||||
text4: '欢迎使用国际化组件!',
|
||||
text5: '渲染一个组件 {testComponent1} {testComponent2}!',
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@ import I18nProvider from './src/core/components/I18nProvider';
|
|||
import injectIntl, { I18nContext, InjectProvider } from './src/core/components/InjectI18n';
|
||||
import useI18n from './src/core/hook/useI18n';
|
||||
import createI18n from './src/core/createI18n';
|
||||
import { MessageDescriptor } from './src/types/interfaces';
|
||||
import { InjectedIntl, MessageDescriptor } from './src/types/interfaces';
|
||||
// 函数API
|
||||
export {
|
||||
I18n,
|
||||
|
@ -36,7 +36,7 @@ export {
|
|||
// 组件
|
||||
export {
|
||||
FormattedMessage,
|
||||
I18nContext as IntlContext,
|
||||
I18nContext,
|
||||
I18nProvider as IntlProvider,
|
||||
injectIntl as injectIntl,
|
||||
InjectProvider as RawIntlProvider,
|
||||
|
@ -64,3 +64,7 @@ export function defineMessages<K extends keyof any, T = MessageDescriptor, U = R
|
|||
export function defineMessage<T>(msg: T): T {
|
||||
return msg;
|
||||
}
|
||||
|
||||
export interface InjectedIntlProps {
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
export default {
|
||||
module.exports = {
|
||||
coverageDirectory: 'coverage',
|
||||
resetModules: true,
|
||||
preset: 'ts-jest/presets/js-with-ts',
|
||||
|
@ -30,10 +30,8 @@ export default {
|
|||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: 'tsconfig.json',
|
||||
diagnostics: false,
|
||||
},
|
||||
},
|
||||
testPathIgnorePatterns: ['\\\\node_modules\\\\'],
|
||||
|
||||
testEnvironment: 'jsdom',
|
||||
};
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
"version": "0.0.5",
|
||||
"description": "",
|
||||
"main": "build/intl.umd.js",
|
||||
"type": "module",
|
||||
"type": "commonjs",
|
||||
"types": "build/@types/index.d.ts",
|
||||
"scripts": {
|
||||
"demo-serve": "webpack serve --mode=development",
|
||||
"build": "rollup --config rollup.config.js && npm run build-types ",
|
||||
"build": "rollup --config rollup.config.js && npm run build-types",
|
||||
"build-types": "tsc -p tsconfig.json && rollup -c build-type.js",
|
||||
"test": "jest --no-cache --config jest.config.js",
|
||||
"test": "jest --config jest.config.js",
|
||||
"test-c": "jest --coverage"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -17,7 +17,8 @@
|
|||
"url": ""
|
||||
},
|
||||
"files": [
|
||||
"/build"
|
||||
"build",
|
||||
"README.md"
|
||||
],
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
@ -26,23 +27,35 @@
|
|||
"openinula": ">=0.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.21.3",
|
||||
"@babel/preset-env": "^7.16.7",
|
||||
"@babel/preset-react": "^7.9.4",
|
||||
"@babel/preset-typescript": "7.16.7",
|
||||
"@rollup/plugin-babel": "^6.0.3",
|
||||
"@rollup/plugin-node-resolve": "^7.1.3",
|
||||
"@rollup/plugin-typescript": "^11.0.0",
|
||||
"rollup-plugin-dts": "^6.1.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@types/node": "^16.18.27",
|
||||
"@types/react": "18.0.25",
|
||||
"babel": "^6.23.0",
|
||||
"babel-jest": "^29.5.0",
|
||||
"babel-loader": "^9.1.2",
|
||||
"html-webpack-plugin": "^5.5.1",
|
||||
"jest": "29.3.1",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"jsdom": "^21.1.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"prettier": "^2.8.7",
|
||||
"rollup": "^2.0.0",
|
||||
"rollup-plugin-livereload": "^2.0.5",
|
||||
"rollup-plugin-serve": "^1.1.0",
|
||||
"rollup-plugin-visualizer": "^5.10.0",
|
||||
"ts-node": "10.9.1",
|
||||
"rollup-plugin-terser": "^5.3.0",
|
||||
"tslib": "^2.6.1",
|
||||
"webpack": "^5.72.1",
|
||||
"ts-jest": "29.0.3",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "4.9.3",
|
||||
"webpack": "^5.81.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.13.3"
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -19,7 +19,6 @@ import babel from '@rollup/plugin-babel';
|
|||
import nodeResolve from '@rollup/plugin-node-resolve';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
@ -30,55 +29,34 @@ const output = path.join(__dirname, '/build');
|
|||
|
||||
const extensions = ['.js', '.ts', '.tsx'];
|
||||
|
||||
const BuildConfig = mode => {
|
||||
const prod = mode.startsWith('prod');
|
||||
const outputList = [
|
||||
export default {
|
||||
input: entry,
|
||||
output: [
|
||||
{
|
||||
file: path.join(output, `cjs/intl.${prod ? 'min.' : ''}js`),
|
||||
sourcemap: 'true',
|
||||
format: 'cjs',
|
||||
globals: {
|
||||
openinula: 'Inula',
|
||||
},
|
||||
},
|
||||
{
|
||||
file: path.join(output, `umd/intl.${prod ? 'min.' : ''}js`),
|
||||
file: path.resolve(output, 'intl.umd.js'),
|
||||
name: 'InulaI18n',
|
||||
sourcemap: 'true',
|
||||
format: 'umd',
|
||||
globals: {
|
||||
openinula: 'Inula',
|
||||
},
|
||||
},
|
||||
];
|
||||
if (!prod) {
|
||||
outputList.push({
|
||||
file: path.join(output, 'esm/intl.js'),
|
||||
sourcemap: 'true',
|
||||
{
|
||||
file: path.resolve(output, 'intl.esm-browser.js'),
|
||||
format: 'esm',
|
||||
});
|
||||
}
|
||||
return {
|
||||
input: entry,
|
||||
output: outputList,
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
extensions,
|
||||
modulesOnly: true,
|
||||
}),
|
||||
babel({
|
||||
exclude: 'node_modules/**',
|
||||
configFile: path.join(__dirname, '/.babelrc'),
|
||||
extensions,
|
||||
babelHelpers: 'runtime',
|
||||
}),
|
||||
typescript({
|
||||
tsconfig: 'tsconfig.json',
|
||||
include: ['./**/*.ts', './**/*.tsx'],
|
||||
}),
|
||||
terser(),
|
||||
],
|
||||
external: ['openinula', 'react', 'react-dom'],
|
||||
};
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
extensions,
|
||||
modulesOnly: true,
|
||||
}),
|
||||
babel({
|
||||
exclude: 'node_modules/**',
|
||||
configFile: path.join(__dirname, '/babel.config.js'),
|
||||
extensions,
|
||||
}),
|
||||
typescript({
|
||||
tsconfig: 'tsconfig.json',
|
||||
include: ['./**/*.ts', './**/*.tsx'],
|
||||
}),
|
||||
terser(),
|
||||
],
|
||||
external: ['openinula', 'react', 'react-dom'],
|
||||
};
|
||||
export default [BuildConfig('dev'), BuildConfig('prod')];
|
||||
|
|
|
@ -18,13 +18,8 @@
|
|||
* \\x[a-fA-F0-9]{2} 匹配形如 \x0A 的十六进制转义字符。
|
||||
* [nrtf'"] 匹配常见的转义字符:\n(换行符)、\r(回车符)、\t(制表符)、\f(换页符)、\'(单引号)和 \"(双引号)。
|
||||
*/
|
||||
export const UNICODE_REG: RegExp = /\\(?:u\{[a-fA-F0-9]+}|x[a-fA-F0-9]{2}|[nrtf'"])/g;
|
||||
export const STICKY_FLAG: string = 'ym';
|
||||
export const GLOBAL_FLAG: string = 'gm';
|
||||
export const UNICODE_REG = /\\(?:u\{[a-fA-F0-9]+}|x[a-fA-F0-9]{2}|[nrtf'"])/g;
|
||||
|
||||
export const VERTICAL_LINE: string = '|';
|
||||
export const UNICODE_FLAG: string = 'u';
|
||||
export const STATE_GROUP_START_INDEX: number = 1;
|
||||
// Inula 需要被保留静态常量
|
||||
export const INULA_STATICS = {
|
||||
childContextTypes: true,
|
||||
|
@ -81,22 +76,3 @@ export const INULA_MEMO_STATICS = {
|
|||
|
||||
// 默认复数规则
|
||||
export const DEFAULT_PLURAL_KEYS = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
||||
|
||||
export const voidElementTags = [
|
||||
'area',
|
||||
'base',
|
||||
'br',
|
||||
'col',
|
||||
'embed',
|
||||
'hr',
|
||||
'img',
|
||||
'input',
|
||||
'keygen',
|
||||
'link',
|
||||
'meta',
|
||||
'param',
|
||||
'source',
|
||||
'track',
|
||||
'wbr',
|
||||
'menuitem',
|
||||
];
|
||||
|
|
|
@ -18,48 +18,30 @@ import DateTimeFormatter from '../format/fomatters/DateTimeFormatter';
|
|||
import NumberFormatter from '../format/fomatters/NumberFormatter';
|
||||
import { getFormatMessage } from '../format/getFormatMessage';
|
||||
import { I18nCache, I18nProps, MessageDescriptor, MessageOptions } from '../types/interfaces';
|
||||
import {
|
||||
Locale,
|
||||
Locales,
|
||||
Messages,
|
||||
AllLocaleConfig,
|
||||
AllMessages,
|
||||
LocaleConfig,
|
||||
Error,
|
||||
Events,
|
||||
InulaNode,
|
||||
} from '../types/types';
|
||||
import { Locale, Locales, Messages, AllLocaleConfig, AllMessages, LocaleConfig, Error, Events } from '../types/types';
|
||||
import creatI18nCache from '../format/cache/cache';
|
||||
import { isValidElement } from 'openinula';
|
||||
|
||||
export class I18n extends EventDispatcher<Events> {
|
||||
public locale: Locale;
|
||||
public locales: Locales;
|
||||
public defaultLocale?: Locale;
|
||||
public timeZone?: string;
|
||||
private allMessages: AllMessages;
|
||||
private readonly _localeConfig: AllLocaleConfig;
|
||||
public readonly onError?: Error;
|
||||
private readonly allMessages: AllMessages;
|
||||
public readonly error?: Error;
|
||||
public readonly cache?: I18nCache;
|
||||
|
||||
constructor(props: I18nProps) {
|
||||
super();
|
||||
this.defaultLocale = 'en';
|
||||
this.locale = this.defaultLocale;
|
||||
this.locale = 'en';
|
||||
this.locales = this.locale || '';
|
||||
this.allMessages = {};
|
||||
this._localeConfig = {};
|
||||
this.onError = props.onError;
|
||||
this.timeZone = '';
|
||||
this.error = props.error;
|
||||
|
||||
this.loadMessage(props.messages);
|
||||
|
||||
if (props.localeConfig) {
|
||||
this.loadLocaleConfig(props.localeConfig);
|
||||
}
|
||||
if (props.messages) {
|
||||
this.changeMessage(props.messages);
|
||||
}
|
||||
|
||||
if (props.locale || props.locales) {
|
||||
this.changeLanguage(props.locale!, props.locales);
|
||||
|
@ -111,11 +93,6 @@ export class I18n extends EventDispatcher<Events> {
|
|||
}
|
||||
}
|
||||
|
||||
changeMessage(messages: AllMessages) {
|
||||
this.allMessages = messages;
|
||||
this.emit('change');
|
||||
}
|
||||
|
||||
// 加载messages
|
||||
loadMessage(localeOrMessages: Locale | AllMessages | undefined, messages?: Messages) {
|
||||
if (messages) {
|
||||
|
@ -141,21 +118,9 @@ export class I18n extends EventDispatcher<Events> {
|
|||
formatMessage(
|
||||
id: MessageDescriptor | string,
|
||||
values: Record<string, unknown> | undefined = {},
|
||||
{ messages, context, formatOptions }: MessageOptions = {}
|
||||
{ message, context, formatOptions }: MessageOptions = {}
|
||||
) {
|
||||
// 在多次渲染时,保证存储component不丢失
|
||||
const components: { [key: string]: InulaNode } = {};
|
||||
const tempValues: Record<string, unknown> = { ...values };
|
||||
if (tempValues) {
|
||||
Object.keys(tempValues).forEach((key, index) => {
|
||||
const value = tempValues[key];
|
||||
if (!isValidElement(value)) return;
|
||||
// 将inula元素暂存
|
||||
components[index] = value;
|
||||
tempValues[key] = `<${index}/>`;
|
||||
});
|
||||
}
|
||||
return getFormatMessage(this, id, tempValues, { messages, context, formatOptions }, components!);
|
||||
return getFormatMessage(this, id, values, { message, context, formatOptions });
|
||||
}
|
||||
|
||||
formatDate(value: string | Date, formatOptions?: Intl.DateTimeFormatOptions): string {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
import { Children, Fragment } from 'openinula';
|
||||
import Inula, { Children, Fragment } from 'openinula';
|
||||
import { FormattedMessageProps } from '../../types/interfaces';
|
||||
import useI18n from '../hook/useI18n';
|
||||
|
||||
|
@ -22,17 +22,28 @@ import useI18n from '../hook/useI18n';
|
|||
* @constructor
|
||||
*/
|
||||
function FormattedMessage(props: FormattedMessageProps) {
|
||||
const { formatMessage } = useI18n();
|
||||
const { id, values, messages, formatOptions, context, tagName: TagName = Fragment, children, comment }: any = props;
|
||||
const { i18n } = useI18n();
|
||||
const {
|
||||
id,
|
||||
values,
|
||||
messages,
|
||||
formatOptions,
|
||||
context,
|
||||
tagName: TagName = Fragment,
|
||||
children,
|
||||
comment,
|
||||
useMemorize,
|
||||
}: any = props;
|
||||
|
||||
const formatMessageOptions = {
|
||||
comment,
|
||||
messages,
|
||||
context,
|
||||
useMemorize,
|
||||
formatOptions,
|
||||
};
|
||||
|
||||
const formattedMessage = formatMessage(id, values, formatMessageOptions);
|
||||
let formattedMessage = i18n.formatMessage(id, values, formatMessageOptions);
|
||||
|
||||
if (typeof children === 'function') {
|
||||
const childNodes = Array.isArray(formattedMessage) ? formattedMessage : [formattedMessage];
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
import { useRef, useState, useEffect, useMemo } from 'openinula';
|
||||
import Inula, { useRef, useState, useEffect, useMemo } from 'openinula';
|
||||
import { InjectProvider } from './InjectI18n';
|
||||
import I18n, { createI18nInstance } from '../I18n';
|
||||
import { AllMessages, I18nProviderProps, Messages } from '../../types/types';
|
||||
import { I18nProviderProps } from '../../types/types';
|
||||
|
||||
/**
|
||||
* 用于为应用程序提供国际化的格式化功能,管理程序中的语言文本信息和本地化资源信息
|
||||
|
@ -23,31 +23,28 @@ import { AllMessages, I18nProviderProps, Messages } from '../../types/types';
|
|||
* @constructor
|
||||
*/
|
||||
const I18nProvider = (props: I18nProviderProps) => {
|
||||
const { locale, messages, children, i18n } = props;
|
||||
const { locale, messages, children } = props;
|
||||
|
||||
const i18nInstance =
|
||||
i18n ||
|
||||
useMemo(() => {
|
||||
return createI18nInstance({
|
||||
locale: locale,
|
||||
messages: messages,
|
||||
});
|
||||
}, [locale, messages]);
|
||||
const i18n = useMemo(() => {
|
||||
return createI18nInstance({
|
||||
locale: locale,
|
||||
messages: messages,
|
||||
});
|
||||
}, [locale, messages]);
|
||||
|
||||
// 使用useRef保存上次的locale值
|
||||
const localeRef = useRef<string | undefined>(i18nInstance.locale);
|
||||
const localeMessage = useRef<string | Messages | AllMessages>(i18nInstance.messages);
|
||||
const [context, setContext] = useState<I18n>(i18nInstance);
|
||||
const localeRef = useRef<string | undefined>(i18n.locale);
|
||||
|
||||
const [context, setContext] = useState<I18n>(i18n);
|
||||
|
||||
useEffect(() => {
|
||||
const handleChange = () => {
|
||||
if (localeRef.current !== i18nInstance.locale || localeMessage.current !== i18nInstance.messages) {
|
||||
localeRef.current = i18nInstance.locale;
|
||||
localeMessage.current = i18nInstance.messages;
|
||||
setContext(i18nInstance);
|
||||
if (localeRef.current !== i18n.locale) {
|
||||
localeRef.current = i18n.locale;
|
||||
setContext(i18n);
|
||||
}
|
||||
};
|
||||
const removeListener = i18nInstance.on('change', handleChange);
|
||||
let removeListener = i18n.on('change', handleChange);
|
||||
|
||||
// 手动触发一次 handleChange,以确保 context 的正确性
|
||||
handleChange();
|
||||
|
@ -56,7 +53,7 @@ const I18nProvider = (props: I18nProviderProps) => {
|
|||
return () => {
|
||||
removeListener();
|
||||
};
|
||||
}, [i18nInstance]);
|
||||
}, [i18n]);
|
||||
|
||||
// 提供一个Provider组件
|
||||
return <InjectProvider value={context}>{children}</InjectProvider>;
|
||||
|
|
|
@ -31,16 +31,13 @@ export const InjectProvider = Provider;
|
|||
function injectI18n(Component, options?: InjectOptions): any {
|
||||
const {
|
||||
isUsingForwardRef = false, // 默认不使用
|
||||
ensureContext = false,
|
||||
} = options || {};
|
||||
|
||||
// 定义一个名为 WrappedI18n 的函数组件,接收传入组件的 props 和 forwardedRef,返回传入组件并注入 i18n
|
||||
const WrappedI18n = props => (
|
||||
<Consumer>
|
||||
{context => {
|
||||
if (ensureContext) {
|
||||
isVariantI18n(context);
|
||||
}
|
||||
isVariantI18n(context);
|
||||
|
||||
const i18nProps = {
|
||||
intl: context,
|
||||
|
|
|
@ -13,29 +13,20 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
import { configProps, I18nCache } from '../types/interfaces';
|
||||
import { createI18nInstance } from './I18n';
|
||||
import I18n, { createI18nInstance } from './I18n';
|
||||
import creatI18nCache from '../format/cache/cache';
|
||||
import { IntlType } from '../types/types';
|
||||
|
||||
/**
|
||||
* createI18n hook函数,用于创建国际化i8n实例,以进行相关的数据操作
|
||||
*/
|
||||
|
||||
export const createI18n = (config: configProps, cache?: I18nCache): IntlType => {
|
||||
export const createI18n = (config: configProps, cache?: I18nCache): I18n => {
|
||||
const { locale, defaultLocale, messages } = config;
|
||||
const i18n = createI18nInstance({
|
||||
locale: locale || defaultLocale || 'en',
|
||||
return createI18nInstance({
|
||||
locale: locale || defaultLocale || 'zh',
|
||||
messages: messages,
|
||||
cache: cache ?? creatI18nCache(),
|
||||
});
|
||||
return {
|
||||
i18n,
|
||||
...config,
|
||||
formatMessage: i18n.formatMessage.bind(i18n),
|
||||
formatNumber: i18n.formatNumber.bind(i18n),
|
||||
formatDate: i18n.formatDate.bind(i18n),
|
||||
$t: i18n.formatMessage.bind(i18n),
|
||||
};
|
||||
};
|
||||
|
||||
export default createI18n;
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
import { useContext, useMemo } from 'openinula';
|
||||
import Inula, { useContext } from 'openinula';
|
||||
import utils from '../../utils/utils';
|
||||
import { I18nContext } from '../components/InjectI18n';
|
||||
import I18n from '../I18n';
|
||||
|
@ -23,22 +23,15 @@ import { IntlType } from '../../types/types';
|
|||
* 使用 useI18n 钩子函数可以更方便地在函数组件中进行国际化操作
|
||||
*/
|
||||
function useI18n(): IntlType {
|
||||
const i18n = useContext<I18n>(I18nContext);
|
||||
utils.isVariantI18n(i18n);
|
||||
return useMemo(() => {
|
||||
return {
|
||||
i18n: i18n,
|
||||
locale: i18n.locale,
|
||||
messages: i18n.messages,
|
||||
defaultLocale: i18n.defaultLocale,
|
||||
timeZone: i18n.timeZone,
|
||||
onError: i18n.onError,
|
||||
formatMessage: i18n.formatMessage.bind(i18n),
|
||||
formatNumber: i18n.formatNumber.bind(i18n),
|
||||
formatDate: i18n.formatDate.bind(i18n),
|
||||
$t: i18n.formatMessage.bind(i18n),
|
||||
};
|
||||
}, [i18n]);
|
||||
const i18nContext = useContext<I18n>(I18nContext);
|
||||
utils.isVariantI18n(i18nContext);
|
||||
const i18n = i18nContext;
|
||||
return {
|
||||
i18n: i18n,
|
||||
formatMessage: i18n.formatMessage.bind(i18n),
|
||||
formatNumber: i18n.formatNumber.bind(i18n),
|
||||
formatDate: i18n.formatDate.bind(i18n),
|
||||
};
|
||||
}
|
||||
|
||||
export default useI18n;
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
import { CompiledMessage, Locale, LocaleConfig, Locales } from '../types/types';
|
||||
import generateFormatters from './generateFormatters';
|
||||
import { FormatOptions, I18nCache } from '../types/interfaces';
|
||||
import creatI18nCache from './cache/cache';
|
||||
import { createIntlCache } from '../../index';
|
||||
|
||||
/**
|
||||
* 获取翻译结果
|
||||
|
@ -28,18 +28,12 @@ class Translation {
|
|||
private readonly localeConfig: Record<string, any>;
|
||||
private readonly cache: I18nCache;
|
||||
|
||||
constructor(
|
||||
compiledMessage: CompiledMessage,
|
||||
locale: Locale,
|
||||
locales: Locales,
|
||||
localeConfig: LocaleConfig,
|
||||
cache?: I18nCache
|
||||
) {
|
||||
constructor(compiledMessage, locale, locales, localeConfig, cache?) {
|
||||
this.compiledMessage = compiledMessage;
|
||||
this.locale = locale;
|
||||
this.locales = locales;
|
||||
this.localeConfig = localeConfig;
|
||||
this.cache = cache ?? creatI18nCache();
|
||||
this.cache = cache ?? createIntlCache;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,7 +53,7 @@ class Translation {
|
|||
const value = values[name];
|
||||
const formatter = formatters[type](value, format);
|
||||
|
||||
let message: any;
|
||||
let message;
|
||||
if (typeof formatter === 'function') {
|
||||
message = formatter(textFormatter); // 递归调用
|
||||
} else {
|
||||
|
@ -74,7 +68,8 @@ class Translation {
|
|||
|
||||
const textFormatter = createTextFormatter(this.locale, this.locales, values, formatOptions, this.localeConfig);
|
||||
// 通过递归方法formatCore进行格式化处理
|
||||
return this.formatMessage(this.compiledMessage, textFormatter); // 返回要格式化的结果
|
||||
const result = this.formatMessage(this.compiledMessage, textFormatter);
|
||||
return result; // 返回要格式化的结果
|
||||
}
|
||||
|
||||
formatMessage(compiledMessage: CompiledMessage, textFormatter: (...args: any[]) => any) {
|
||||
|
|
|
@ -17,7 +17,7 @@ import utils from '../../utils/utils';
|
|||
import NumberFormatter from './NumberFormatter';
|
||||
import { Locale, Locales } from '../../types/types';
|
||||
import { I18nCache } from '../../types/interfaces';
|
||||
import creatI18nCache from '../cache/cache';
|
||||
import { createIntlCache } from '../../../index';
|
||||
|
||||
/**
|
||||
* 复数格式化
|
||||
|
@ -29,12 +29,12 @@ class PluralFormatter {
|
|||
private readonly message: any;
|
||||
private readonly cache: I18nCache;
|
||||
|
||||
constructor(locale: Locale, locales: Locales, value: any, message: any, cache?:I18nCache) {
|
||||
constructor(locale, locales, value, message, cache?) {
|
||||
this.locale = locale;
|
||||
this.locales = locales;
|
||||
this.value = value;
|
||||
this.message = message;
|
||||
this.cache = cache ?? creatI18nCache();
|
||||
this.cache = cache ?? createIntlCache();
|
||||
}
|
||||
|
||||
// 将 message中的“#”替换为指定数字value,并返回新的字符串或者字符串数组
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
*/
|
||||
|
||||
import utils from '../../utils/utils';
|
||||
import {Locale, SelectPool} from '../../types/types';
|
||||
import { Locale } from '../../types/types';
|
||||
import { I18nCache } from '../../types/interfaces';
|
||||
|
||||
/**
|
||||
|
@ -26,12 +26,12 @@ class SelectFormatter {
|
|||
private readonly locale: Locale;
|
||||
private readonly cache: I18nCache;
|
||||
|
||||
constructor(locale: Locale, cache: I18nCache) {
|
||||
constructor(locale, cache) {
|
||||
this.locale = locale;
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
getRule(value: SelectPool, rules: any) {
|
||||
getRule(value, rules) {
|
||||
if (this.cache.select) {
|
||||
// 创建key,用于唯一标识
|
||||
const cacheKey = utils.generateKey<Intl.NumberFormatOptions>(this.locale, rules);
|
||||
|
|
|
@ -19,23 +19,25 @@ import { DatePool, Locale, Locales, SelectPool } from '../types/types';
|
|||
import PluralFormatter from './fomatters/PluralFormatter';
|
||||
import SelectFormatter from './fomatters/SelectFormatter';
|
||||
import { FormatOptions, I18nCache, IntlMessageFormat } from '../types/interfaces';
|
||||
import cache from './cache/cache';
|
||||
|
||||
/**
|
||||
* 默认格式化接口
|
||||
*/
|
||||
const generateFormatters = (
|
||||
locale: Locale,
|
||||
locale: Locale | Locales,
|
||||
locales: Locales,
|
||||
localeConfig: Record<string, any> = { plurals: undefined },
|
||||
formatOptions: FormatOptions = {}, // 自定义格式对象
|
||||
cache: I18nCache
|
||||
): IntlMessageFormat => {
|
||||
locale = locales || locale;
|
||||
const { plurals } = localeConfig;
|
||||
/**
|
||||
* 样式函数 ,根据格式获取格式样式, 如货币百分比, 返回相应的格式的对象,如果没有设定格式,则返回一个空对象
|
||||
* @param formatOption
|
||||
*/
|
||||
const getStyleOption = (formatOption: string | number) => {
|
||||
const getStyleOption = formatOption => {
|
||||
if (typeof formatOption === 'string') {
|
||||
return formatOptions[formatOption] || { option: formatOption };
|
||||
} else {
|
||||
|
@ -56,14 +58,14 @@ const generateFormatters = (
|
|||
return pluralFormatter.replaceSymbol.bind(pluralFormatter);
|
||||
},
|
||||
|
||||
selectordinal: (value: number, { offset = 0, ...rules }) => {
|
||||
selectordinal: (value: number, { offset = 0, ...rules }, useMemorize?) => {
|
||||
const message = rules[value] || rules[(plurals as any)?.(value - offset, true)] || rules.other;
|
||||
const pluralFormatter = new PluralFormatter(locale, locales, value - offset, message, cache);
|
||||
const pluralFormatter = new PluralFormatter(locale, locales, value - offset, message, useMemorize);
|
||||
return pluralFormatter.replaceSymbol.bind(pluralFormatter);
|
||||
},
|
||||
|
||||
// 选择规则,如果规则对象中包含与该值相对应的属性,则返回该属性的值;否则,返回 "other" 属性的值。
|
||||
select: (value: SelectPool, formatRules: any) => {
|
||||
select: (value: SelectPool, formatRules) => {
|
||||
const selectFormatter = new SelectFormatter(locale, cache);
|
||||
return selectFormatter.getRule(value, formatRules);
|
||||
},
|
||||
|
@ -73,16 +75,17 @@ const generateFormatters = (
|
|||
return new NumberFormatter(locales, getStyleOption(formatOption), cache).numberFormat(value);
|
||||
},
|
||||
|
||||
// 用于将日期格式化为字符串,接受一个日期对象和一个格式化规则。它会根据规则返回格式化后的字符串。
|
||||
/**
|
||||
* 用于将日期格式化为字符串,接受一个日期对象和一个格式化规则。它会根据规则返回格式化后的字符串。
|
||||
* eg: { year: 'numeric', month: 'long', day: 'numeric' } 是一个用于指定DateTimeFormatter如何将日期对象转换为字符串的参数。
|
||||
* \year: 'numeric' 表示年份的表示方式是数字形式(比如2023)。
|
||||
* month: 'long' 表示月份的表示方式是全名(比如January)。
|
||||
* day: 'numeric' 表示日期的表示方式是数字形式(比如1号)。
|
||||
* @param value
|
||||
* @param formatOption { year: 'numeric', month: 'long', day: 'numeric' }
|
||||
* @param useMemorize
|
||||
*/
|
||||
dateTimeFormat: (value: DatePool, formatOption: any) => {
|
||||
dateTimeFormat: (value: DatePool, formatOption) => {
|
||||
return new DateTimeFormatter(locales, getStyleOption(formatOption), cache).dateTimeFormat(value, formatOption);
|
||||
},
|
||||
|
||||
|
|
|
@ -19,21 +19,19 @@ import I18n from '../core/I18n';
|
|||
import { MessageDescriptor, MessageOptions } from '../types/interfaces';
|
||||
import { CompiledMessage } from '../types/types';
|
||||
import creatI18nCache from './cache/cache';
|
||||
import { formatElements } from '../utils/formatElements';
|
||||
|
||||
export function getFormatMessage(
|
||||
i18n: I18n,
|
||||
id: MessageDescriptor | string,
|
||||
values: Record<string, unknown> | undefined = {},
|
||||
options: MessageOptions = {},
|
||||
components: any
|
||||
options: MessageOptions = {}
|
||||
) {
|
||||
let { messages, context } = options;
|
||||
let { message, context } = options;
|
||||
const { formatOptions } = options;
|
||||
const cache = i18n.cache ?? creatI18nCache();
|
||||
if (typeof id !== 'string') {
|
||||
values = values || id.defaultValues;
|
||||
messages = id.messages || id.defaultMessage;
|
||||
message = id.message || id.defaultMessage;
|
||||
context = id.context;
|
||||
id = id.id;
|
||||
}
|
||||
|
@ -44,7 +42,7 @@ export function getFormatMessage(
|
|||
const messageUnavailable = isMissingContextMessage || isMissingMessage;
|
||||
|
||||
// 对错误消息进行处理
|
||||
const messageError = i18n.onError;
|
||||
const messageError = i18n.error;
|
||||
if (messageError && messageUnavailable) {
|
||||
if (typeof messageError === 'function') {
|
||||
return messageError(i18n.locale, id, context);
|
||||
|
@ -55,17 +53,14 @@ export function getFormatMessage(
|
|||
|
||||
let compliedMessage: CompiledMessage;
|
||||
if (context) {
|
||||
compliedMessage = i18n.messages[context][id] || messages || id;
|
||||
compliedMessage = i18n.messages[context][id] || message || id;
|
||||
} else {
|
||||
compliedMessage = i18n.messages[id] || messages || id;
|
||||
compliedMessage = i18n.messages[id] || message || id;
|
||||
}
|
||||
|
||||
// 对解析的message进行parse解析,并输出解析后的Token
|
||||
// 对解析的messages进行parse解析,并输出解析后的Token
|
||||
compliedMessage = typeof compliedMessage === 'string' ? utils.compile(compliedMessage) : compliedMessage;
|
||||
|
||||
const translation = new Translation(compliedMessage, i18n.locale, i18n.locales, i18n.localeConfig, cache);
|
||||
const formatResult = translation.translate(values, formatOptions);
|
||||
|
||||
// 如果存在inula元素,则返回包含格式化的Inula元素的数组
|
||||
return formatElements(formatResult, components);
|
||||
return translation.translate(values, formatOptions);
|
||||
}
|
||||
|
|
|
@ -16,12 +16,9 @@
|
|||
import ruleUtils from '../utils/parseRuleUtils';
|
||||
import { LexerInterface } from '../types/interfaces';
|
||||
|
||||
/**
|
||||
* 词法解析器,主要根据设计的规则对message进行处理成Token
|
||||
*/
|
||||
class Lexer<T> implements LexerInterface<T> {
|
||||
readonly startState: string;
|
||||
readonly unionReg: Record<string, any>;
|
||||
readonly states: Record<string, any>;
|
||||
private buffer = '';
|
||||
private stack: string[] = [];
|
||||
private index = 0;
|
||||
|
@ -31,23 +28,19 @@ class Lexer<T> implements LexerInterface<T> {
|
|||
private state = '';
|
||||
private groups: string[] = [];
|
||||
private error: Record<string, any> | undefined;
|
||||
private regexp: any;
|
||||
private regexp;
|
||||
private fast: Record<string, unknown> = {};
|
||||
private queuedGroup: string | null = '';
|
||||
private value = '';
|
||||
|
||||
constructor(unionReg: Record<string, any>, startState: string) {
|
||||
this.startState = startState;
|
||||
this.unionReg = unionReg;
|
||||
this.states = unionReg;
|
||||
this.buffer = '';
|
||||
this.stack = [];
|
||||
this.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据新的消息重置解析器
|
||||
* @param data 消息数据
|
||||
*/
|
||||
public reset(data?: string) {
|
||||
this.buffer = data || '';
|
||||
this.index = 0;
|
||||
|
@ -64,7 +57,7 @@ class Lexer<T> implements LexerInterface<T> {
|
|||
return;
|
||||
}
|
||||
this.state = state;
|
||||
const info = this.unionReg[state];
|
||||
const info = this.states[state];
|
||||
this.groups = info.groups;
|
||||
this.error = info.error;
|
||||
this.regexp = info.regexp;
|
||||
|
@ -80,7 +73,7 @@ class Lexer<T> implements LexerInterface<T> {
|
|||
this.setState(state);
|
||||
}
|
||||
|
||||
private getGroup(match: Record<string, object>) {
|
||||
private getGroup(match: Record<string, any>) {
|
||||
const groupCount = this.groups.length;
|
||||
for (let i = 0; i < groupCount; i++) {
|
||||
if (match[i + 1] !== undefined) {
|
||||
|
@ -94,9 +87,7 @@ class Lexer<T> implements LexerInterface<T> {
|
|||
return this.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 迭代获取下一个 token
|
||||
*/
|
||||
// 迭代获取下一个 token
|
||||
public next() {
|
||||
const index = this.index;
|
||||
|
||||
|
@ -121,6 +112,7 @@ class Lexer<T> implements LexerInterface<T> {
|
|||
const regexp = this.regexp;
|
||||
regexp.lastIndex = index;
|
||||
const match = getMatch(regexp, buffer);
|
||||
|
||||
const error = this.error;
|
||||
if (match == null) {
|
||||
return this.getToken(error, buffer.slice(index, buffer.length), index);
|
||||
|
@ -139,9 +131,9 @@ class Lexer<T> implements LexerInterface<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取Token
|
||||
* @param group 解析模板后获得的属性值
|
||||
* @param text 文本属性的信息
|
||||
* 獲取Token
|
||||
* @param group 解析模板后獲得的屬性值
|
||||
* @param text 文本屬性的信息
|
||||
* @param offset 偏移量
|
||||
* @private
|
||||
*/
|
||||
|
@ -195,7 +187,7 @@ class Lexer<T> implements LexerInterface<T> {
|
|||
return token;
|
||||
}
|
||||
|
||||
// 增加迭代器,允许逐个访问集合中的元素方法
|
||||
// 增加迭代器
|
||||
[Symbol.iterator]() {
|
||||
return {
|
||||
next: (): IteratorResult<T> => {
|
||||
|
@ -206,15 +198,9 @@ class Lexer<T> implements LexerInterface<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据正则表达式,获取匹配到message的值
|
||||
* 索引为 0 的元素是完整的匹配结果。
|
||||
* 索引为 1、2、3 等的元素是正则表达式中指定的捕获组的匹配结果。
|
||||
*/
|
||||
const getMatch = ruleUtils.checkSticky()
|
||||
? // 正则表达式具有 sticky 标志
|
||||
(regexp: any, buffer: string) => regexp.exec(buffer)
|
||||
(regexp, buffer) => regexp.exec(buffer)
|
||||
: // 正则表达式具有 global 标志,匹配的字符串长度为 0,则表示匹配失败
|
||||
(regexp: any, buffer: string) => (regexp.exec(buffer)[0].length === 0 ? null : regexp.exec(buffer));
|
||||
|
||||
(regexp, buffer) => (regexp.exec(buffer)[0].length === 0 ? null : regexp.exec(buffer));
|
||||
export default Lexer;
|
||||
|
|
|
@ -14,47 +14,40 @@
|
|||
*/
|
||||
|
||||
const body: Record<string, any> = {
|
||||
doubleapos: { match: "''", value: () => "'" },
|
||||
doubleapos: { match: '\'\'', value: () => '\'' },
|
||||
quoted: {
|
||||
lineBreaks: true,
|
||||
match: /'[{}#](?:[^]*?[^'])?'(?!')/u, // 用以匹配单引号、花括号{}以及井号# 如'Hello' 、{name}、{}#
|
||||
value: (src: string) => src.slice(1, -1).replace(/''/g, "'"),
|
||||
match: /'[{}#](?:[^]*?[^'])?'(?!')/u,
|
||||
value: src => src.slice(1, -1).replace(/''/g, '\''),
|
||||
},
|
||||
argument: {
|
||||
lineBreaks: true,
|
||||
|
||||
// 用于匹配{name、{Hello{World,匹配{ }花括号中有任何Unicode字符,如空格、制表符等
|
||||
match: /\{\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*/u,
|
||||
push: 'arg',
|
||||
value: (src: string) => src.substring(1).trim(),
|
||||
value: src => src.substring(1).trim(),
|
||||
},
|
||||
octothorpe: '#',
|
||||
end: { match: '}', pop: 1 },
|
||||
content: {
|
||||
lineBreaks: true,
|
||||
match: /[^][^{}#]*/u, // 主要匹配不包含[]任何字符(除了换行符)、不包含{}、#的任何个字符
|
||||
},
|
||||
content: { lineBreaks: true, match: /[^][^{}#']*/u },
|
||||
};
|
||||
|
||||
const arg: Record<string, any> = {
|
||||
select: {
|
||||
lineBreaks: true,
|
||||
match: /,\s*(?:plural|select|selectordinal)\s*,\s*/u, // 匹配内容包含 plural、select 或 selectordinal
|
||||
next: 'select', // 继续解析下一个参数
|
||||
value: (src: string) => src.split(',')[1].trim(), // 提取第二个参数,并处理收尾空格
|
||||
match: /,\s*(?:plural|select|selectordinal)\s*,\s*/u,
|
||||
next: 'select',
|
||||
value: src => src.split(',')[1].trim(),
|
||||
},
|
||||
'func-args': {
|
||||
// 匹配是否包含其他非特殊字符的参数,匹配结果包含特殊字符,如param1, param2, param3
|
||||
lineBreaks: true,
|
||||
match: /,\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*,/u,
|
||||
next: 'body',
|
||||
value: (src: string) => src.split(',')[1].trim(), // 参数字符串去除逗号并去除首尾空格
|
||||
value: src => src.split(',')[1].trim(),
|
||||
},
|
||||
'func-simple': {
|
||||
// 匹配是否包含其他简单参数,匹配结果不包含标点符号:param1 param2 param3
|
||||
lineBreaks: true,
|
||||
match: /,\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*/u,
|
||||
value: (src: string) => src.substring(1).trim(),
|
||||
value: src => src.substring(1).trim(),
|
||||
},
|
||||
end: { match: '}', pop: 1 },
|
||||
};
|
||||
|
@ -62,17 +55,14 @@ const arg: Record<string, any> = {
|
|||
const select: Record<string, any> = {
|
||||
offset: {
|
||||
lineBreaks: true,
|
||||
match: /\s*offset\s*:\s*\d+\s*/u, // 匹配message中是否包含偏移量offest信息
|
||||
value: (src: string) => src.split(':')[1].trim(),
|
||||
match: /\s*offset\s*:\s*\d+\s*/u,
|
||||
value: src => src.split(':')[1].trim(),
|
||||
},
|
||||
case: {
|
||||
// 检查匹配该行是否包含分支信息。
|
||||
lineBreaks: true,
|
||||
|
||||
// 设置规则匹配以左大括号 { 结尾的字符串,以等号 = 后跟数字开头的字符串,或者以非特殊符号和非空白字符开头的字符串,如 '=1 {'
|
||||
match: /\s*(?:=\d+|[^\p{Pat_Syn}\p{Pat_WS}]+)\s*\{/u,
|
||||
push: 'body', // 匹配成功,则会push到body栈中
|
||||
value: (src: string) => src.substring(0, src.indexOf('{')).trim(),
|
||||
push: 'body',
|
||||
value: src => src.substring(0, src.indexOf('{')).trim(),
|
||||
},
|
||||
end: { match: /\s*\}/u, pop: 1 },
|
||||
};
|
||||
|
|
|
@ -17,13 +17,12 @@ import Lexer from './Lexer';
|
|||
import { mappingRule } from './mappingRule';
|
||||
import ruleUtils from '../utils/parseRuleUtils';
|
||||
import { RawToken } from '../types/types';
|
||||
import { STATE_GROUP_START_INDEX, GLOBAL_FLAG, STICKY_FLAG, UNICODE_FLAG, VERTICAL_LINE } from '../constants';
|
||||
|
||||
const defaultErrorRule = ruleUtils.getRuleOptions('error', { lineBreaks: true, shouldThrow: true });
|
||||
|
||||
// 解析规则并生成词法分析器所需的数据结构,以便进行词法分析操作
|
||||
function parseRules(rules: Record<string, any>, hasStates: boolean): Record<string, object> {
|
||||
let errorRule: Record<string, object> | null = null;
|
||||
function parseRules(rules: Record<string, any>, hasStates: boolean): Record<string, any> {
|
||||
let errorRule: Record<string, any> | null = null;
|
||||
const fast: Record<string, unknown> = {};
|
||||
let enableFast = true;
|
||||
let unicodeFlag: boolean | null = null;
|
||||
|
@ -59,7 +58,7 @@ function parseRules(rules: Record<string, any>, hasStates: boolean): Record<stri
|
|||
|
||||
groups.push(options);
|
||||
|
||||
// 检查是否所有规则都使用了unicode,或者都未使用
|
||||
// 检查是否所有规则都使用了 unicode 标志,或者都未使用
|
||||
unicodeFlag = checkUnicode(match, unicodeFlag, options);
|
||||
|
||||
const pat = ruleUtils.getRegUnion(match.map(ruleUtils.getReg));
|
||||
|
@ -82,11 +81,11 @@ function parseRules(rules: Record<string, any>, hasStates: boolean): Record<stri
|
|||
|
||||
// 如果没有 fallback 规则,则使用 sticky 标志,只在当前索引位置寻找匹配项,如果不支持 sticky 标志,则使用无法被否定的空模式来模拟
|
||||
const fallbackRule = errorRule && errorRule.fallback;
|
||||
let flags = ruleUtils.checkSticky() && !fallbackRule ? STICKY_FLAG : GLOBAL_FLAG;
|
||||
const suffix = ruleUtils.checkSticky() || fallbackRule ? '' : VERTICAL_LINE;
|
||||
let flags = ruleUtils.checkSticky() && !fallbackRule ? 'ym' : 'gm';
|
||||
const suffix = ruleUtils.checkSticky() || fallbackRule ? '' : '|';
|
||||
|
||||
if (unicodeFlag === true) {
|
||||
flags += UNICODE_FLAG;
|
||||
flags += 'u';
|
||||
}
|
||||
const combined = new RegExp(ruleUtils.getRegUnion(parts) + suffix, flags);
|
||||
|
||||
|
@ -98,18 +97,18 @@ function parseRules(rules: Record<string, any>, hasStates: boolean): Record<stri
|
|||
};
|
||||
}
|
||||
|
||||
export function checkStateGroup(group: Record<string, any>, name: string, mappingRules: Record<string, object>) {
|
||||
export function checkStateGroup(group: Record<string, any>, name: string, map: Record<string, any>) {
|
||||
const state = group && (group.push || group.next);
|
||||
if (state && !mappingRules[state]) {
|
||||
if (state && !map[state]) {
|
||||
throw new Error('The state is missing.');
|
||||
}
|
||||
if (group && group.pop && +group.pop !== STATE_GROUP_START_INDEX) {
|
||||
if (group && group.pop && +group.pop !== 1) {
|
||||
throw new Error('The value of pop must be 1.');
|
||||
}
|
||||
}
|
||||
|
||||
// 将国际化解析规则注入分词器中
|
||||
function parseMappingRule(mappingRule: Record<string, object>, startState?: string): Lexer<RawToken> {
|
||||
function parseMappingRule(mappingRule: Record<string, any>, startState?: string): Lexer<RawToken> {
|
||||
const keys = Object.getOwnPropertyNames(mappingRule);
|
||||
|
||||
if (!startState) {
|
||||
|
@ -134,7 +133,7 @@ function parseMappingRule(mappingRule: Record<string, object>, startState?: stri
|
|||
continue;
|
||||
}
|
||||
|
||||
const splice = [j, STATE_GROUP_START_INDEX];
|
||||
const splice = [j, 1];
|
||||
if (rule.include !== key && !included[rule.include]) {
|
||||
included[rule.include] = true;
|
||||
const newRules = ruleMap[rule.include];
|
||||
|
@ -175,30 +174,17 @@ function parseMappingRule(mappingRule: Record<string, object>, startState?: stri
|
|||
});
|
||||
});
|
||||
|
||||
// 将规则注入到词法解析器
|
||||
return new Lexer(mappingAllRules, startState);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速匹配模式
|
||||
* @param match
|
||||
* @param fast
|
||||
* @param options
|
||||
*/
|
||||
function processFast(match: Record<string, any>, fast: Record<string, unknown> = {}, options: Record<string, object>) {
|
||||
function processFast(match, fast: Record<string, unknown>, options) {
|
||||
while (match.length && typeof match[0] === 'string' && match[0].length === 1) {
|
||||
// 获取到数组的第一个元素
|
||||
const word = match.shift();
|
||||
fast[word.charCodeAt(0)] = options;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用以处理错误逻辑
|
||||
* @param options 操作属性
|
||||
* @param errorRule 错误规则
|
||||
*/
|
||||
function handleErrorRule(options: Record<string, object>, errorRule: Record<string, object>) {
|
||||
function handleErrorRule(options, errorRule: Record<string, any>) {
|
||||
if (!options.fallback === !errorRule.fallback) {
|
||||
throw new Error('errorRule can only set one!');
|
||||
} else {
|
||||
|
@ -206,13 +192,7 @@ function handleErrorRule(options: Record<string, object>, errorRule: Record<stri
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用以检查message中是否包含Unicode
|
||||
* @param match 匹配到的message
|
||||
* @param unicodeFlag Unicode标志
|
||||
* @param options 操作属性
|
||||
*/
|
||||
function checkUnicode(match: Record<string, any>, unicodeFlag: boolean | null, options: Record<string, any>) {
|
||||
function checkUnicode(match, unicodeFlag, options) {
|
||||
for (let j = 0; j < match.length; j++) {
|
||||
const obj = match[j];
|
||||
if (!ruleUtils.checkRegExp(obj)) {
|
||||
|
@ -221,16 +201,14 @@ function checkUnicode(match: Record<string, any>, unicodeFlag: boolean | null, o
|
|||
|
||||
if (unicodeFlag === null) {
|
||||
unicodeFlag = obj.unicode;
|
||||
} else {
|
||||
if (unicodeFlag !== obj.unicode && options.fallback === false) {
|
||||
throw new Error('If the /u flag is used, all!');
|
||||
}
|
||||
} else if (unicodeFlag !== obj.unicode && options.fallback === false) {
|
||||
throw new Error('If the /u flag is used, all!');
|
||||
}
|
||||
}
|
||||
return unicodeFlag;
|
||||
}
|
||||
|
||||
function checkStateOptions(hasStates: boolean, options: Record<string, any>) {
|
||||
function checkStateOptions(hasStates: boolean, options) {
|
||||
if (!hasStates) {
|
||||
throw new Error('State toggle options are not allowed in stateless tokenizers!');
|
||||
}
|
||||
|
@ -239,11 +217,6 @@ function checkStateOptions(hasStates: boolean, options: Record<string, any>) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在fallback属性,用以来判断快速匹配规则
|
||||
* @param rules
|
||||
* @param enableFast
|
||||
*/
|
||||
function isExistsFallback(rules: Record<string, any>, enableFast: boolean) {
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
if (rules[i].fallback) {
|
||||
|
@ -253,7 +226,7 @@ function isExistsFallback(rules: Record<string, any>, enableFast: boolean) {
|
|||
return enableFast;
|
||||
}
|
||||
|
||||
function isOptionsErrorOrFallback(options: Record<string, object>, errorRule: Record<string, object> | null) {
|
||||
function isOptionsErrorOrFallback(options, errorRule: Record<string, any> | null) {
|
||||
if (options.error || options.fallback) {
|
||||
// 只能设置一个 errorRule
|
||||
if (errorRule) {
|
||||
|
|
|
@ -14,13 +14,23 @@
|
|||
*/
|
||||
|
||||
import { lexer } from './parseMappingRule';
|
||||
import { RawToken } from '../types/types';
|
||||
import { RawToken, Token } from '../types/types';
|
||||
import { DEFAULT_PLURAL_KEYS } from '../constants';
|
||||
import { Content, FunctionArg, PlainArg, Select, TokenContext } from '../types/interfaces';
|
||||
import Lexer from './Lexer';
|
||||
|
||||
const getContext = (lt: Record<string, any>): TokenContext => ({
|
||||
offset: lt.offset,
|
||||
line: lt.line,
|
||||
col: lt.col,
|
||||
text: lt.text,
|
||||
lineNum: lt.lineBreaks,
|
||||
});
|
||||
|
||||
export const checkSelectType = (value: string): boolean => {
|
||||
return value === 'plural' || value === 'select' || value === 'selectordinal';
|
||||
};
|
||||
|
||||
/**
|
||||
* 语法解析器,根据Token,获得具备上下文的AST
|
||||
*/
|
||||
class Parser {
|
||||
cardinalKeys: string[] = DEFAULT_PLURAL_KEYS;
|
||||
ordinalKeys: string[] = DEFAULT_PLURAL_KEYS;
|
||||
|
@ -29,7 +39,7 @@ class Parser {
|
|||
lexer.reset(message);
|
||||
}
|
||||
|
||||
isSelectKeyValid(type: Select['type'], value: string) {
|
||||
isSelectKeyValid(token: RawToken, type: Select['type'], value: string) {
|
||||
if (value[0] === '=') {
|
||||
if (type === 'select') {
|
||||
throw new Error('The key value of the select type is invalid.');
|
||||
|
@ -65,7 +75,7 @@ class Parser {
|
|||
break;
|
||||
}
|
||||
case 'case': {
|
||||
this.isSelectKeyValid(type, token.value);
|
||||
this.isSelectKeyValid(token, type, token.value);
|
||||
select.cases.push({
|
||||
key: token.value.replace(/=/g, ''),
|
||||
tokens: this.parse(isPlural),
|
||||
|
@ -84,11 +94,6 @@ class Parser {
|
|||
throw new Error('The message end position is invalid.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析获得的Token
|
||||
* @param token
|
||||
* @param isPlural
|
||||
*/
|
||||
parseToken(token: RawToken, isPlural: boolean): PlainArg | FunctionArg | Select {
|
||||
const context = getContext(token);
|
||||
const nextToken = lexer.next();
|
||||
|
@ -148,12 +153,7 @@ class Parser {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析方法入口
|
||||
* 在根级别解析时,遇到结束符号即结束解析并返回结果;而在非根级别解析时,遇到结束符号会被视为不合法的结束位置,抛出错误
|
||||
* @param isPlural 标记复数
|
||||
* @param isRoot 标记根节点
|
||||
*/
|
||||
// 在根级别解析时,遇到结束符号即结束解析并返回结果;而在非根级别解析时,遇到结束符号会被视为不合法的结束位置,抛出错误
|
||||
parse(isPlural: boolean, isRoot?: boolean): Array<Content | PlainArg | FunctionArg | Select> {
|
||||
const tokens: any[] = [];
|
||||
let content: string | Content | null = null;
|
||||
|
@ -201,23 +201,6 @@ class Parser {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得 Token 的上下文
|
||||
* @param Token Token
|
||||
*/
|
||||
const getContext = (Token: RawToken): TokenContext => ({
|
||||
offset: Token.offset,
|
||||
line: Token.line,
|
||||
col: Token.col,
|
||||
text: Token.text,
|
||||
lineNum: Token.lineBreaks,
|
||||
});
|
||||
|
||||
// 用以检查select规则中的类型
|
||||
export const checkSelectType = (value: string): boolean => {
|
||||
return value === 'plural' || value === 'select' || value === 'selectordinal';
|
||||
};
|
||||
|
||||
export default function parse(message: string): Array<Content | PlainArg | FunctionArg | Select> {
|
||||
const parser = new Parser(message);
|
||||
return parser.parse(false, true);
|
||||
|
|
|
@ -13,25 +13,15 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import {
|
||||
AllLocaleConfig,
|
||||
AllMessages,
|
||||
Locale,
|
||||
Locales,
|
||||
Error,
|
||||
DatePool,
|
||||
SelectPool,
|
||||
RawToken,
|
||||
InulaNode,
|
||||
} from './types';
|
||||
import { AllLocaleConfig, AllMessages, Locale, Locales, Error, DatePool, SelectPool, RawToken } from './types';
|
||||
import I18n from '../core/I18n';
|
||||
import Lexer from '../parser/Lexer';
|
||||
import { InulaElement, Key } from 'openinula';
|
||||
|
||||
// FormattedMessage的参数定义
|
||||
export interface FormattedMessageProps extends MessageDescriptor {
|
||||
values?: Record<string, unknown>;
|
||||
tagName?: string;
|
||||
|
||||
children?(nodes: any[]): any;
|
||||
}
|
||||
|
||||
|
@ -44,7 +34,7 @@ export interface MessageDescriptor extends MessageOptions {
|
|||
|
||||
export interface MessageOptions {
|
||||
comment?: string;
|
||||
messages?: string;
|
||||
message?: string;
|
||||
context?: string;
|
||||
formatOptions?: FormatOptions;
|
||||
}
|
||||
|
@ -58,26 +48,15 @@ export interface I18nCache {
|
|||
octothorpe: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface RichText {
|
||||
components?: { [key: string]: InulaNode };
|
||||
}
|
||||
|
||||
export interface InulaPortal extends InulaElement {
|
||||
key: Key | null;
|
||||
children: InulaNode;
|
||||
}
|
||||
|
||||
// I18n类的传参
|
||||
export type I18nProps = RichText & {
|
||||
export interface I18nProps {
|
||||
locale?: Locale;
|
||||
locales?: Locales;
|
||||
messages?: AllMessages;
|
||||
defaultLocale?: string;
|
||||
timeZone?: string;
|
||||
localeConfig?: AllLocaleConfig;
|
||||
cache?: I18nCache;
|
||||
onError?: Error;
|
||||
};
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
// 消息格式化选项类型
|
||||
export interface FormatOptions {
|
||||
|
@ -95,13 +74,16 @@ export interface I18nContextProps {
|
|||
i18n?: I18n;
|
||||
}
|
||||
|
||||
export type configProps = I18nProps & {
|
||||
export interface configProps {
|
||||
locale?: Locale;
|
||||
messages?: AllMessages;
|
||||
defaultLocale?: string;
|
||||
RenderOnLocaleChange?: boolean;
|
||||
children?: any;
|
||||
onWarn?: Error;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IntlMessageFormat {
|
||||
export interface IntlMessageFormat extends configProps, MessageOptions {
|
||||
plural: (
|
||||
value: number,
|
||||
{
|
||||
|
@ -222,6 +204,7 @@ export interface InjectedIntl {
|
|||
formatMessage(
|
||||
messageDescriptor: MessageDescriptor,
|
||||
values?: Record<string, unknown>,
|
||||
options?: MessageOptions
|
||||
): string | any[];
|
||||
options?: MessageOptions,
|
||||
useMemorize?: boolean
|
||||
): string;
|
||||
}
|
||||
|
|
|
@ -23,17 +23,16 @@ import {
|
|||
I18nContextProps,
|
||||
configProps,
|
||||
InjectedIntl,
|
||||
InulaPortal,
|
||||
} from './interfaces';
|
||||
import { InulaElement } from 'openinula';
|
||||
import I18n from '../core/I18n';
|
||||
|
||||
export type Error = string | ((message: any, id: any, context: any) => string);
|
||||
export type Error = string | ((message, id, context) => string);
|
||||
|
||||
export type Locale = string;
|
||||
|
||||
export type Locales = Locale | Locale[];
|
||||
|
||||
export type LocaleConfig = { plurals?: (...args: any[]) => any };
|
||||
export type LocaleConfig = { plurals?: (...arg: any) => any };
|
||||
|
||||
export type AllLocaleConfig = Record<Locale, LocaleConfig>;
|
||||
|
||||
|
@ -60,7 +59,7 @@ export type Token = Content | PlainArg | FunctionArg | Select | Octothorpe;
|
|||
|
||||
export type DatePool = Date | string;
|
||||
|
||||
export type SelectPool = string | number;
|
||||
export type SelectPool = string | Record<string, unknown>;
|
||||
|
||||
export type RawToken = {
|
||||
type: string;
|
||||
|
@ -75,23 +74,13 @@ export type RawToken = {
|
|||
|
||||
export type I18nProviderProps = I18nContextProps & configProps;
|
||||
|
||||
export type IntlType = I18nContextProps & {
|
||||
defaultLocale?: string | undefined;
|
||||
onError?: Error | undefined;
|
||||
messages?:
|
||||
| string
|
||||
| Record<string, string>
|
||||
| Record<string, string | CompiledMessagePart[]>
|
||||
| Record<string, Record<string, string> | Record<string, string | CompiledMessagePart[]>>;
|
||||
locale?: string;
|
||||
export type IntlType = {
|
||||
i18n: I18n;
|
||||
formatMessage: (...args: any[]) => any;
|
||||
formatNumber: (...args: any[]) => any;
|
||||
formatDate: (...args: any[]) => any;
|
||||
$t?: (...args: any[]) => any;
|
||||
};
|
||||
|
||||
export type InjectedIntlProps = {
|
||||
export interface InjectedIntlProps {
|
||||
intl: InjectedIntl;
|
||||
};
|
||||
|
||||
export type InulaNode = InulaElement | string | number | Iterable<InulaNode> | InulaPortal | boolean | null | undefined;
|
||||
}
|
||||
|
|
|
@ -1,107 +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.
|
||||
*/
|
||||
|
||||
import { cloneElement, createElement, Fragment, InulaElement } from 'openinula';
|
||||
import { voidElementTags } from '../constants';
|
||||
|
||||
// 用于匹配标签的正则表达式
|
||||
const tagReg = /<(\d+)>(.*?)<\/\1>|<(\d+)\/>/;
|
||||
|
||||
// 用于匹配换行符的正则表达式
|
||||
const nlReg = /(?:\r\n|\r|\n)/g;
|
||||
|
||||
export function formatElements(
|
||||
value: string,
|
||||
elements: { [key: string]: InulaElement<any> } = {}
|
||||
): string | Array<any> {
|
||||
const elementKeyID = getElementIndex(0, '$Inula');
|
||||
|
||||
// value:This is a rich text with a custom component: <1/>
|
||||
const arrays = value.replace(nlReg, '').split(tagReg);
|
||||
|
||||
// 若无InulaNode元素,则返回
|
||||
if (arrays.length === 1) return value;
|
||||
|
||||
const result: any = [];
|
||||
|
||||
const before = arrays.shift();
|
||||
if (before) {
|
||||
result.push(before);
|
||||
}
|
||||
|
||||
for (const [index, children, after] of getElements(arrays)) {
|
||||
let element = elements[index];
|
||||
|
||||
if (!element || (voidElementTags[element.type as string] && children)) {
|
||||
const errorMessage = !element
|
||||
? `Index not declared as ${index} in original translation`
|
||||
: `${element.type} , No child element exists. Please check.`;
|
||||
console.error(errorMessage);
|
||||
|
||||
// 对于异常元素,通过创建<></>来代替,并继续解析现有的子元素和之后的元素,并保证在构建数组时,不会因为缺少元素而导致索引错位。
|
||||
element = createElement(Fragment, {});
|
||||
}
|
||||
|
||||
// 如果存在子元素,则进行递归处理
|
||||
const formattedChildren = children ? formatElements(children, elements) : element.props.children;
|
||||
|
||||
// 更新element 的属性和子元素
|
||||
const clonedElement = cloneElement(element, { key: elementKeyID() }, formattedChildren);
|
||||
result.push(clonedElement);
|
||||
|
||||
if (after) {
|
||||
result.push(after);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从arrays数组中解析出标签元素和其子元素
|
||||
* @param arrays
|
||||
*/
|
||||
function getElements(arrays: string[]) {
|
||||
// 如果 arrays 数组为空,则返回空数组
|
||||
if (!arrays.length) return [];
|
||||
|
||||
/**
|
||||
* pairedIndex: 第一个元素表示配对标签的内容,即 <1>...</1> 形式的标签。
|
||||
* children: 第二个元素表示配对标签内的子元素内容。
|
||||
* unpairedIndex: 第三个元素表示自闭合标签的内容,即 <1/> 形式的标签。
|
||||
* textAfter: 第四个元素表示标签之后的文本内容,即标签后紧跟着的文本。
|
||||
* eg: [undefined,undefined,1,""]
|
||||
*/
|
||||
const [pairedIndex, children, unpairedIndex, textAfter] = arrays.splice(0, 4);
|
||||
|
||||
// 解析当前标签元素和它的子元素,返回一个包含标签索引、子元素和后续文本的数组
|
||||
const currentElement: [number, string, string] = [
|
||||
parseInt(pairedIndex || unpairedIndex), // 解析标签索引,如果是自闭合标签,则使用 unpaired
|
||||
children || '',
|
||||
textAfter || '',
|
||||
];
|
||||
|
||||
// 递归调用 getElements 函数,处理剩余的 arrays 数组
|
||||
const remainingElements = getElements(arrays);
|
||||
|
||||
// 将当前元素和递归处理后的元素数组合并并返回
|
||||
return [currentElement, ...remainingElements];
|
||||
}
|
||||
|
||||
// 对传入富文本元素的位置标志索引
|
||||
function getElementIndex(count = 0, prefix = '') {
|
||||
return function () {
|
||||
return `${prefix}_${count++}`;
|
||||
};
|
||||
}
|
|
@ -18,7 +18,6 @@ function getType(input: any): string {
|
|||
return str.slice(8, -1).toLowerCase();
|
||||
}
|
||||
|
||||
// 类型检查器
|
||||
const createTypeChecker = (type: string) => {
|
||||
return (input: any) => {
|
||||
return getType(input) === type.toLowerCase();
|
||||
|
@ -29,25 +28,24 @@ const checkObject = (input: any) => input !== null && typeof input === 'object';
|
|||
|
||||
const checkRegExp = createTypeChecker('RegExp');
|
||||
|
||||
// 使用正则表达式,如果对象存在则访问该属性,用来判断当前环境是否支持正则表达式sticky属性。
|
||||
const checkSticky = () => typeof new RegExp('')?.sticky === 'boolean';
|
||||
|
||||
// 转义正则表达式中的特殊字符
|
||||
function transferReg(str: string): string {
|
||||
function transferReg(s: string): string {
|
||||
// eslint-disable-next-line
|
||||
return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
}
|
||||
|
||||
// 计算正则表达式中捕获组的数量,用以匹配()
|
||||
function getRegGroups(str: string): number {
|
||||
const regExp = new RegExp('|' + str);
|
||||
// 计算正则表达式中捕获组的数量
|
||||
function getRegGroups(s: string): number {
|
||||
const re = new RegExp('|' + s);
|
||||
// eslint-disable-next-line
|
||||
return regExp.exec('')?.length! - 1;
|
||||
return re.exec('')?.length! - 1;
|
||||
}
|
||||
|
||||
// 创建一个捕获组的正则表达式模式
|
||||
function getRegCapture(str: string): string {
|
||||
return '(' + str + ')';
|
||||
function getRegCapture(s: string): string {
|
||||
return '(' + s + ')';
|
||||
}
|
||||
|
||||
// 将正则表达式合并为一个联合的正则表达式模式
|
||||
|
@ -55,7 +53,7 @@ function getRegUnion(regexps: string[]): string {
|
|||
if (!regexps.length) {
|
||||
return '(?!)';
|
||||
}
|
||||
const source = regexps.map(str => '(?:' + str + ')').join('|');
|
||||
const source = regexps.map(s => '(?:' + s + ')').join('|');
|
||||
return '(?:' + source + ')';
|
||||
}
|
||||
|
||||
|
@ -145,7 +143,7 @@ function getRulesByArray(array: any[]) {
|
|||
return result;
|
||||
}
|
||||
|
||||
function getRuleOptions(type: any, obj: any) {
|
||||
function getRuleOptions(type, obj) {
|
||||
// 如果 obj 不是一个对象,则将其转换为包含 'match' 属性的对象
|
||||
if (!checkObject(obj)) {
|
||||
obj = { match: obj };
|
||||
|
@ -184,23 +182,23 @@ function getRuleOptions(type: any, obj: any) {
|
|||
} else {
|
||||
options.match = [];
|
||||
}
|
||||
options.match.sort((str1: string, str2: string) => {
|
||||
options.match.sort((a, b) => {
|
||||
// 根据规则的类型进行排序,确保正则表达式排在最前面,长度较长的规则排在前面
|
||||
if (checkRegExp(str1) && checkRegExp(str2)) {
|
||||
if (checkRegExp(a) && checkRegExp(b)) {
|
||||
return 0;
|
||||
} else if (checkRegExp(str2)) {
|
||||
} else if (checkRegExp(b)) {
|
||||
return -1;
|
||||
} else if (checkRegExp(str1)) {
|
||||
} else if (checkRegExp(a)) {
|
||||
return +1;
|
||||
} else {
|
||||
return str2.length - str1.length;
|
||||
return b.length - a.length;
|
||||
}
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function getRules(spec: any) {
|
||||
function getRules(spec) {
|
||||
return Array.isArray(spec) ? getRulesByArray(spec) : getRulesByObject(spec);
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ function compile(message: string): CompiledMessage {
|
|||
try {
|
||||
return getTokenAST(parse(message));
|
||||
} catch (e) {
|
||||
console.error(`Message cannot be parse due to syntax errors: ${message},cause by ${e}`);
|
||||
console.error(`Message cannot be parse due to syntax errors: ${message}`);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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 I18n from '../../src/core/I18n';
|
||||
|
||||
describe('I18n', () => {
|
||||
it('load catalog and merge with existing', () => {
|
||||
const i18n = new I18n({});
|
||||
const messages = {
|
||||
Hello: 'Hello',
|
||||
};
|
||||
|
||||
i18n.loadMessage('en', messages);
|
||||
i18n.changeLanguage('en');
|
||||
expect(i18n.messages).toEqual(messages);
|
||||
i18n.loadMessage('fr', { Hello: 'Salut' });
|
||||
expect(i18n.messages).toEqual(messages);
|
||||
});
|
||||
|
||||
it('should load multiple language ', function () {
|
||||
const enMessages = {
|
||||
Hello: 'Hello',
|
||||
};
|
||||
const frMessage = {
|
||||
Hello: 'Salut',
|
||||
};
|
||||
const intl = new I18n({});
|
||||
intl.loadMessage({
|
||||
en: enMessages,
|
||||
fr: frMessage,
|
||||
});
|
||||
intl.changeLanguage('en');
|
||||
expect(intl.messages).toEqual(enMessages);
|
||||
|
||||
intl.changeLanguage('fr');
|
||||
expect(intl.messages).toEqual(frMessage);
|
||||
});
|
||||
|
||||
it('should switch active locale', () => {
|
||||
const messages = {
|
||||
Hello: 'Salut',
|
||||
};
|
||||
|
||||
const i18n = new I18n({
|
||||
locale: 'en',
|
||||
messages: {
|
||||
fr: messages,
|
||||
en: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(i18n.locale).toEqual('en');
|
||||
expect(i18n.messages).toEqual({});
|
||||
|
||||
i18n.changeLanguage('fr');
|
||||
expect(i18n.locale).toEqual('fr');
|
||||
expect(i18n.messages).toEqual(messages);
|
||||
});
|
||||
|
||||
it('should switch active locale', () => {
|
||||
const messages = {
|
||||
Hello: 'Salut',
|
||||
};
|
||||
|
||||
const i18n = new I18n({
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: messages,
|
||||
fr: {},
|
||||
},
|
||||
});
|
||||
|
||||
i18n.changeLanguage('en');
|
||||
expect(i18n.locale).toEqual('en');
|
||||
expect(i18n.messages).toEqual(messages);
|
||||
i18n.changeLanguage('fr');
|
||||
expect(i18n.locale).toEqual('fr');
|
||||
expect(i18n.messages).toEqual({});
|
||||
});
|
||||
it('._ allow escaping syntax characters', () => {
|
||||
const messages = {
|
||||
'My \'\'name\'\' is \'{name}\'': 'Mi \'\'nombre\'\' es \'{name}\'',
|
||||
};
|
||||
const i18n = new I18n({
|
||||
locale: 'es',
|
||||
messages: { es: messages },
|
||||
});
|
||||
expect(i18n.formatMessage('My \'\'name\'\' is \'{name}\'')).toEqual('Mi \'nombre\' es {name}');
|
||||
});
|
||||
|
||||
it('._ should format message from catalog', function () {
|
||||
const messages = {
|
||||
Hello: 'Salut',
|
||||
id: 'Je m\'appelle {name}',
|
||||
};
|
||||
const i18n = new I18n({
|
||||
locale: 'fr',
|
||||
messages: { fr: messages },
|
||||
});
|
||||
expect(i18n.locale).toEqual('fr');
|
||||
expect(i18n.formatMessage('Hello')).toEqual('Salut');
|
||||
expect(i18n.formatMessage('id', { name: 'Fred' })).toEqual('Je m\'appelle Fred');
|
||||
});
|
||||
|
||||
it('should return the formatted date and time', () => {
|
||||
const i18n = new I18n({
|
||||
locale: 'fr',
|
||||
});
|
||||
const formattedDateTime = i18n.formatDate('2023-06-06T07:53:54.465Z', {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'short',
|
||||
});
|
||||
expect(typeof formattedDateTime).toBe('string');
|
||||
expect(formattedDateTime).toEqual('mardi 6 juin 2023 à 15:53');
|
||||
});
|
||||
|
||||
it('should return the formatted number', () => {
|
||||
const i18n = new I18n({
|
||||
locale: 'en',
|
||||
});
|
||||
const formattedNumber = i18n.formatNumber(123456.789, { style: 'currency', currency: 'USD' });
|
||||
expect(typeof formattedNumber).toBe('string');
|
||||
expect(formattedNumber).toEqual('$123,456.79');
|
||||
});
|
||||
});
|
|
@ -1,270 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import I18n from '../../src/core/I18n';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/';
|
||||
|
||||
// 测试组件
|
||||
const IndividualCustomComponent = () => {
|
||||
return <span>Custom Component</span>;
|
||||
};
|
||||
|
||||
const CustomComponent = (props: any) => {
|
||||
return <div>{props.children}</div>;
|
||||
};
|
||||
|
||||
const CustomComponentChildren = (props: any) => {
|
||||
return <div>{props.children}</div>;
|
||||
};
|
||||
|
||||
describe('I18n', () => {
|
||||
it('load catalog and merge with existing', () => {
|
||||
const i18n = new I18n({});
|
||||
const messages = {
|
||||
Hello: 'Hello',
|
||||
};
|
||||
|
||||
i18n.loadMessage('en', messages);
|
||||
i18n.changeLanguage('en');
|
||||
expect(i18n.messages).toEqual(messages);
|
||||
i18n.loadMessage('fr', { Hello: 'Salut' });
|
||||
expect(i18n.messages).toEqual(messages);
|
||||
i18n.changeMessage({ Hello: 'Salut' });
|
||||
expect(i18n.messages).toEqual({ Hello: 'Salut' });
|
||||
});
|
||||
|
||||
it('should load multiple language ', function () {
|
||||
const enMessages = {
|
||||
Hello: 'Hello',
|
||||
};
|
||||
const frMessage = {
|
||||
Hello: 'Salut',
|
||||
};
|
||||
const intl = new I18n({});
|
||||
intl.loadMessage({
|
||||
en: enMessages,
|
||||
fr: frMessage,
|
||||
});
|
||||
intl.changeLanguage('en');
|
||||
expect(intl.messages).toEqual(enMessages);
|
||||
|
||||
intl.changeLanguage('fr');
|
||||
expect(intl.messages).toEqual(frMessage);
|
||||
});
|
||||
|
||||
it('should switch active locale', () => {
|
||||
const messages = {
|
||||
Hello: 'Salut',
|
||||
};
|
||||
|
||||
const i18n = new I18n({
|
||||
locale: 'en',
|
||||
messages: {
|
||||
fr: messages,
|
||||
en: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(i18n.locale).toEqual('en');
|
||||
expect(i18n.messages).toEqual({});
|
||||
|
||||
i18n.changeLanguage('fr');
|
||||
expect(i18n.locale).toEqual('fr');
|
||||
expect(i18n.messages).toEqual(messages);
|
||||
});
|
||||
|
||||
it('should switch active locale', () => {
|
||||
const messages = {
|
||||
Hello: 'Salut',
|
||||
};
|
||||
|
||||
const i18n = new I18n({
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: messages,
|
||||
fr: {},
|
||||
},
|
||||
});
|
||||
|
||||
i18n.changeLanguage('en');
|
||||
expect(i18n.locale).toEqual('en');
|
||||
expect(i18n.messages).toEqual(messages);
|
||||
i18n.changeLanguage('fr');
|
||||
expect(i18n.locale).toEqual('fr');
|
||||
expect(i18n.messages).toEqual({});
|
||||
});
|
||||
it('._ allow escaping syntax characters', () => {
|
||||
const messages = {
|
||||
"My ''name'' is '{name}'": "Mi ''nombre'' es '{name}'",
|
||||
};
|
||||
const i18n = new I18n({
|
||||
locale: 'es',
|
||||
messages: { es: messages },
|
||||
});
|
||||
expect(i18n.formatMessage("My ''name'' is '{name}'")).toEqual("Mi ''nombre'' es '{name}'");
|
||||
});
|
||||
|
||||
it('._ should format message from catalog', function () {
|
||||
const messages = {
|
||||
Hello: 'Salut',
|
||||
id: "Je m'appelle {name}",
|
||||
};
|
||||
const i18n = new I18n({
|
||||
locale: 'fr',
|
||||
messages: { fr: messages },
|
||||
});
|
||||
expect(i18n.locale).toEqual('fr');
|
||||
expect(i18n.formatMessage('Hello')).toEqual('Salut');
|
||||
expect(i18n.formatMessage('id', { name: 'Fred' })).toEqual("Je m'appelle Fred");
|
||||
});
|
||||
|
||||
it('should return information with html element', () => {
|
||||
const messages = {
|
||||
id: 'hello, {name}',
|
||||
};
|
||||
const i18n = new I18n({
|
||||
locale: 'es',
|
||||
messages: { es: messages },
|
||||
});
|
||||
const value = '<strong>Jane</strong>';
|
||||
expect(i18n.formatMessage({ id: 'id' }, { name: value })).toEqual('hello, <strong>Jane</strong>');
|
||||
});
|
||||
|
||||
it('test demo from product', () => {
|
||||
const messages = {
|
||||
id: "服务商名称长度不能超过64个字符,允许输入中文、字母、数字、字符_-!@#$^.+'}{',且不能为关键字null(不区分大小写)。",
|
||||
};
|
||||
const i18n = new I18n({
|
||||
locale: 'zh',
|
||||
messages: { zh: messages },
|
||||
});
|
||||
expect(i18n.formatMessage('id')).toEqual(
|
||||
"服务商名称长度不能超过64个字符,允许输入中文、字母、数字、字符_-!@#$^.+'}{',且不能为关键字null(不区分大小写)。"
|
||||
);
|
||||
});
|
||||
|
||||
it('Should return information with dom element', () => {
|
||||
const messages = {
|
||||
richText: 'This is a rich text with a custom component: {customComponent}',
|
||||
};
|
||||
const i18n = new I18n({
|
||||
locale: 'es',
|
||||
messages: { es: messages },
|
||||
});
|
||||
const values = {
|
||||
customComponent: <IndividualCustomComponent />,
|
||||
};
|
||||
const formattedMessage = i18n.formatMessage({ id: 'richText' }, values);
|
||||
|
||||
// 渲染格式化后的文本内容
|
||||
const { getByText } = render(<div>{formattedMessage}</div>);
|
||||
|
||||
// 检查文本内容中是否包含自定义组件的内容
|
||||
expect(getByText('This is a rich text with a custom component')).toContain(
|
||||
'This is a rich text with a custom component'
|
||||
);
|
||||
});
|
||||
|
||||
it('Should return information for nested scenes with dom elements', () => {
|
||||
const messages = {
|
||||
richText: 'This is a rich text with a custom component: {customComponent}',
|
||||
msg: 'test',
|
||||
};
|
||||
const i18n = new I18n({
|
||||
locale: 'es',
|
||||
messages: { es: messages },
|
||||
});
|
||||
const values = {
|
||||
customComponent: (
|
||||
<CustomComponent style={{ margin: '0 4px' }} text={'123'}>
|
||||
<CustomComponentChildren>{i18n.formatMessage({ id: 'msg' })}</CustomComponentChildren>
|
||||
</CustomComponent>
|
||||
),
|
||||
};
|
||||
const formattedMessage = i18n.formatMessage({ id: 'richText' }, values);
|
||||
|
||||
// 渲染格式化后的文本内容
|
||||
const { getByText } = render(<div>{formattedMessage}</div>);
|
||||
|
||||
// 检查文本内容中是否包含自定义组件的内容
|
||||
expect(getByText('test')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Should return information for nested scenes with dom elements', () => {
|
||||
const messages = {
|
||||
richText: 'This is a rich text with a custom component: {customComponent}',
|
||||
msg: 'test',
|
||||
};
|
||||
const i18n = new I18n({
|
||||
locale: 'es',
|
||||
messages: { es: messages },
|
||||
});
|
||||
const values = {
|
||||
customComponent: (
|
||||
<CustomComponent style={{ margin: '0 4px' }} text={'123'}>
|
||||
{i18n.formatMessage({ id: 'msg' })}
|
||||
</CustomComponent>
|
||||
),
|
||||
};
|
||||
const formattedMessage = i18n.formatMessage({ id: 'richText' }, values);
|
||||
|
||||
// 渲染格式化后的文本内容
|
||||
const { getByText } = render(<div>{formattedMessage}</div>);
|
||||
|
||||
// 检查文本内容中是否包含自定义组件的内容
|
||||
expect(getByText('test')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should be returned as value when Multiple dom elements\n', () => {
|
||||
const messages = {
|
||||
richText: '{today}, my name is {name}, and {age} years old!',
|
||||
};
|
||||
const i18n = new I18n({
|
||||
locale: 'es',
|
||||
messages: { es: messages },
|
||||
});
|
||||
const Name = () => {
|
||||
return <span>tom</span>;
|
||||
};
|
||||
const Age = () => {
|
||||
return <span>16</span>;
|
||||
};
|
||||
const Today = () => {
|
||||
return <span>3月2日</span>;
|
||||
};
|
||||
const values = {
|
||||
today: <Today />,
|
||||
name: <Name />,
|
||||
age: <Age />,
|
||||
};
|
||||
const formattedMessage = i18n.formatMessage({ id: 'richText' }, values);
|
||||
|
||||
// 渲染格式化后的文本内容
|
||||
const { getByText } = render(<div>{formattedMessage}</div>);
|
||||
|
||||
// 检查文本内容中是否包含自定义组件的内容
|
||||
expect(getByText('my name is tom, and 16 years old!')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return the formatted date and time', () => {
|
||||
const i18n = new I18n({
|
||||
locale: 'fr',
|
||||
});
|
||||
const formattedDateTime = i18n.formatDate('2023-06-06T07:53:54.465Z', {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'short',
|
||||
});
|
||||
expect(typeof formattedDateTime).toBe('string');
|
||||
expect(formattedDateTime).toEqual('mardi 6 juin 2023 à 15:53');
|
||||
});
|
||||
|
||||
it('should return the formatted number', () => {
|
||||
const i18n = new I18n({
|
||||
locale: 'en',
|
||||
});
|
||||
const formattedNumber = i18n.formatNumber(123456.789, { style: 'currency', currency: 'USD' });
|
||||
expect(typeof formattedNumber).toBe('string');
|
||||
expect(formattedNumber).toEqual('$123,456.79');
|
||||
});
|
||||
});
|
|
@ -43,7 +43,7 @@ describe('<FormattedMessage>', () => {
|
|||
);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(getByTestId('id').textContent).toEqual(i18n.formatMessage('hello', {}, {}));
|
||||
expect(getByTestId('id')).toHaveTextContent(i18n.formatMessage('hello', '', {}));
|
||||
}, 1000);
|
||||
});
|
||||
it('should format context', function () {
|
||||
|
@ -58,6 +58,6 @@ describe('<FormattedMessage>', () => {
|
|||
</span>
|
||||
</I18nProvider>
|
||||
);
|
||||
expect(getByTestId('id').textContent).toEqual(i18n.formatMessage('id', { name: 'fred' }, {}));
|
||||
expect(getByTestId('id')).toHaveTextContent(i18n.formatMessage('id', { name: 'fred' }, {}));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -42,6 +42,7 @@ describe('InjectIntl', () => {
|
|||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const Injected = injectIntl(Wrapped);
|
||||
|
||||
// @ts-ignore
|
||||
expect(() => render(<Injected />)).toThrow("Cannot read properties of null (reading 'i18n')");
|
||||
});
|
||||
|
||||
|
@ -52,7 +53,7 @@ describe('InjectIntl', () => {
|
|||
};
|
||||
|
||||
const { getByTestId } = mountWithProvider(<Injected {...props} />);
|
||||
expect(JSON.stringify(getByTestId('test'))).toEqual(
|
||||
expect(getByTestId('test')).toHaveTextContent(
|
||||
'{"_events":{},"locale":"en","locales":["en"],"allMessages":{},"_localeData":{}}'
|
||||
);
|
||||
});
|
||||
|
|
|
@ -29,20 +29,6 @@ describe('createI18n', () => {
|
|||
).toBe('bar');
|
||||
});
|
||||
|
||||
it('createIntl', function () {
|
||||
const i18n = createI18n({
|
||||
locale: 'en',
|
||||
messages: {
|
||||
test: 'test',
|
||||
},
|
||||
});
|
||||
expect(
|
||||
i18n.$t({
|
||||
id: 'test',
|
||||
})
|
||||
).toBe('test');
|
||||
});
|
||||
|
||||
it('should not warn when defaultRichTextElements is not used', function () {
|
||||
const onWarn = jest.fn();
|
||||
createI18n({
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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 * as React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider, useIntl } from '../../../index';
|
||||
|
||||
const FunctionComponent = ({ spy }: { spy?: Function }) => {
|
||||
const { i18n } = useIntl();
|
||||
spy!(i18n.locale);
|
||||
return null;
|
||||
};
|
||||
|
||||
const FC = () => {
|
||||
const i18n = useIntl();
|
||||
return i18n.formatNumber(10000, { style: 'currency', currency: 'USD' }) as any;
|
||||
};
|
||||
|
||||
describe('useIntl() hooks', () => {
|
||||
it('throws when <IntlProvider> is missing from ancestry', () => {
|
||||
// So it doesn't spam the console
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
expect(() => render(<FunctionComponent />)).toThrow('I18n object is not found!');
|
||||
});
|
||||
|
||||
it('hooks onto the intl context', () => {
|
||||
const spy = jest.fn();
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<FunctionComponent spy={spy} />
|
||||
</IntlProvider>
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith('en');
|
||||
});
|
||||
|
||||
it('should work when switching locale on provider', () => {
|
||||
const { rerender, getByTestId } = render(
|
||||
<IntlProvider locale="en">
|
||||
<span data-testid="comp">
|
||||
<FC />
|
||||
</span>
|
||||
</IntlProvider>
|
||||
);
|
||||
expect(getByTestId('comp')).toMatchSnapshot();
|
||||
rerender(
|
||||
<IntlProvider locale="es">
|
||||
<span data-testid="comp">
|
||||
<FC />
|
||||
</span>
|
||||
</IntlProvider>
|
||||
);
|
||||
expect(getByTestId('comp')).toMatchSnapshot();
|
||||
rerender(
|
||||
<IntlProvider locale="en">
|
||||
<span data-testid="comp">
|
||||
<FC />
|
||||
</span>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(getByTestId('comp')).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -15,7 +15,7 @@
|
|||
import creatI18nCache from '../../../src/format/cache/cache';
|
||||
|
||||
describe('creatI18nCache', () => {
|
||||
it('should create an empty I18nCache object', () => {
|
||||
it('should create an empty IntlCache object', () => {
|
||||
const intlCache = creatI18nCache();
|
||||
|
||||
expect(intlCache).toEqual({
|
||||
|
|
|
@ -61,7 +61,7 @@ describe('DateTimeFormatter', () => {
|
|||
expect(spy).toHaveBeenCalledWith('en-GB', { month: 'short' });
|
||||
});
|
||||
|
||||
it('should not memoize formatter instances when cache is effective', () => {
|
||||
it('should not memoize formatter instances when memoize is false', () => {
|
||||
const spy = jest.spyOn(Intl, 'DateTimeFormat');
|
||||
const formatter1 = new DateTimeFormatter('en-US', { month: 'short' });
|
||||
const formatter2 = new DateTimeFormatter('en-US', { month: 'short' });
|
||||
|
@ -91,7 +91,7 @@ describe('DateTimeFormatter', () => {
|
|||
expect(formatted).toEqual('January 1, 2023');
|
||||
});
|
||||
|
||||
it('should format using memorized formatter when cache is effective', () => {
|
||||
it('should format using memorized formatter when useMemorize is true', () => {
|
||||
const formatter = new DateTimeFormatter('en-US', { year: 'numeric' }, creatI18nCache());
|
||||
const date = new Date(2023, 0, 1);
|
||||
const formatted1 = formatter.dateTimeFormat(date);
|
||||
|
|
|
@ -24,7 +24,7 @@ describe('getFormatMessage', () => {
|
|||
},
|
||||
},
|
||||
locale: 'en',
|
||||
onError: 'missingMessage',
|
||||
error: 'missingMessage',
|
||||
});
|
||||
|
||||
it('should return the correct translation for an existing message ID', () => {
|
||||
|
@ -32,7 +32,7 @@ describe('getFormatMessage', () => {
|
|||
const values = { name: 'John' };
|
||||
const expectedResult = 'Hello, John!';
|
||||
|
||||
const result = getFormatMessage(i18nInstance, id, values, {}, {});
|
||||
const result = getFormatMessage(i18nInstance, id, values);
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
@ -41,7 +41,7 @@ describe('getFormatMessage', () => {
|
|||
const id = 'missingMessage';
|
||||
const expectedResult = 'missingMessage';
|
||||
|
||||
const result = getFormatMessage(i18nInstance, id, {}, {}, {});
|
||||
const result = getFormatMessage(i18nInstance, id);
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
import copyStaticProps from '../../src/utils/copyStaticProps';
|
||||
|
||||
describe('copyStaticProps', () => {
|
||||
it('should hoist static properties from sourceComponent to targetComponent', () => {
|
||||
test('should hoist static properties from sourceComponent to targetComponent', () => {
|
||||
class SourceComponent {
|
||||
static staticProp = 'sourceProp';
|
||||
}
|
||||
|
@ -23,10 +23,11 @@ describe('copyStaticProps', () => {
|
|||
class TargetComponent {}
|
||||
|
||||
copyStaticProps(TargetComponent, SourceComponent);
|
||||
|
||||
expect((TargetComponent as any).staticProp).toBe('sourceProp');
|
||||
});
|
||||
|
||||
it('should hoist static properties from inherited components', () => {
|
||||
test('should hoist static properties from inherited components', () => {
|
||||
class SourceComponent {
|
||||
static staticProp = 'sourceProp';
|
||||
}
|
||||
|
@ -36,10 +37,11 @@ describe('copyStaticProps', () => {
|
|||
class TargetComponent {}
|
||||
|
||||
copyStaticProps(TargetComponent, InheritedComponent);
|
||||
|
||||
expect((TargetComponent as any).staticProp).toBe('sourceProp');
|
||||
});
|
||||
|
||||
it('should not hoist properties if descriptor is not valid', () => {
|
||||
test('should not hoist properties if descriptor is not valid', () => {
|
||||
class SourceComponent {
|
||||
get staticProp() {
|
||||
return 'sourceProp';
|
||||
|
@ -49,10 +51,11 @@ describe('copyStaticProps', () => {
|
|||
class TargetComponent {}
|
||||
|
||||
copyStaticProps(TargetComponent, SourceComponent);
|
||||
|
||||
expect((TargetComponent as any).staticProp).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not hoist properties if descriptor is not valid', () => {
|
||||
test('should not hoist properties if descriptor is not valid', () => {
|
||||
class SourceComponent {
|
||||
static get staticProp() {
|
||||
return 'sourceProp';
|
||||
|
@ -62,10 +65,11 @@ describe('copyStaticProps', () => {
|
|||
class TargetComponent {}
|
||||
|
||||
copyStaticProps(TargetComponent, SourceComponent);
|
||||
|
||||
expect((TargetComponent as any).staticProp).toBe('sourceProp');
|
||||
});
|
||||
|
||||
it('copyStaticProps should not copy static properties that already exist in target or source component', () => {
|
||||
test('copyStaticProps should not copy static properties that already exist in target or source component', () => {
|
||||
const targetComponent = { staticProp: 'target' };
|
||||
const sourceComponent = { staticProp: 'source' };
|
||||
copyStaticProps(targetComponent, sourceComponent);
|
||||
|
|
|
@ -43,7 +43,7 @@ describe('eventEmitter', () => {
|
|||
expect(listener).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("should do nothing when even doesn't exist", () => {
|
||||
it('should do nothing when even doesn\'t exist', () => {
|
||||
const unknown = jest.fn();
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
"declaration": true,
|
||||
"experimentalDecorators": true,
|
||||
"downlevelIteration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"declarationDir": "./build/@types",
|
||||
// 赋值为空数组使@types/node不会起作用
|
||||
"lib": [
|
||||
|
@ -55,8 +54,7 @@
|
|||
}
|
||||
},
|
||||
"include": [
|
||||
"./index.ts",
|
||||
|
||||
"./index.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
|
|
@ -12,18 +12,16 @@
|
|||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
import path from 'path';
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const { resolve } = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
const entryPath = './example/index.tsx';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
export default {
|
||||
entry: path.join(__dirname, entryPath),
|
||||
module.exports = {
|
||||
entry: resolve(__dirname, entryPath),
|
||||
output: {
|
||||
path: path.join(__dirname, './build'),
|
||||
path: resolve(__dirname, './build'),
|
||||
filename: 'main.js',
|
||||
},
|
||||
module: {
|
||||
|
@ -52,7 +50,7 @@ export default {
|
|||
mode: isDevelopment ? 'development' : 'production',
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.join(__dirname, './example/index.html'),
|
||||
template: resolve(__dirname, './example/index.html'),
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
|
|
|
@ -14,5 +14,11 @@
|
|||
*/
|
||||
|
||||
module.exports = {
|
||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }], ['@babel/preset-typescript']],
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{ targets: { node: 'current' }},
|
||||
],
|
||||
['@babel/preset-typescript'],
|
||||
],
|
||||
};
|
||||
|
|
|
@ -16,24 +16,26 @@
|
|||
</div>
|
||||
<script src="../../dist/bundle.js"></script>
|
||||
<script>
|
||||
import inulaRequest from "../../index";
|
||||
|
||||
const sendRequestButton = document.getElementById('sendRequestButton');
|
||||
const cancelRequestButton = document.getElementById('cancelRequestButton');
|
||||
const message = document.getElementById('message');
|
||||
|
||||
let controller = new AbortController();
|
||||
let controller = new AbortController;
|
||||
const signal = controller.signal;
|
||||
|
||||
sendRequestButton.addEventListener('click', function() {
|
||||
setInterval(() => {
|
||||
setInterval(function() {
|
||||
inulaRequest.get('http://localhost:3001/data', {
|
||||
signal
|
||||
}).then(function(response) {
|
||||
message.innerHTML = '请求成功: ' + JSON.stringify(response.data, null, 2);
|
||||
}).catch(function(error) {
|
||||
if (inulaRequest.isCancel(error)) {
|
||||
message.innerHTML = '请求已被取消:' + error.message;
|
||||
message.innerHTML = '请求已被取消: ' + error.message;
|
||||
} else {
|
||||
message.innerHTML = '请求出错:' + error.message;
|
||||
message.innerHTML = '请求出错: ' + error.message;
|
||||
}
|
||||
});
|
||||
}, 1000)
|
||||
|
|
|
@ -118,7 +118,7 @@
|
|||
putResult.innerHTML = JSON.stringify(error, null, 2);
|
||||
});
|
||||
|
||||
inulaRequest.delete('http://localhost:3001/users', {params: {id: 1, test:['{}']}})
|
||||
inulaRequest.delete('http://localhost:3001/users', {params: {id: 1}})
|
||||
.then(function (response) {
|
||||
deleteResult.innerHTML = JSON.stringify(response.data, null, 2);
|
||||
})
|
||||
|
|
|
@ -13,11 +13,11 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import bodyParser from 'body-parser';
|
||||
import cors from 'cors';
|
||||
import express from "express";
|
||||
import * as fs from "fs";
|
||||
import bodyParser from "body-parser";
|
||||
import cors from "cors";
|
||||
import * as path from "path";
|
||||
|
||||
const app = express();
|
||||
const port = 3001;
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
*/
|
||||
|
||||
import inulaRequest from './src/inulaRequest';
|
||||
import useIR from './src/core/useIR/useIR';
|
||||
|
||||
const {
|
||||
create,
|
||||
|
@ -59,6 +60,7 @@ export {
|
|||
isIrError,
|
||||
spread,
|
||||
IrHeaders,
|
||||
useIR,
|
||||
// 兼容axios
|
||||
Axios,
|
||||
AxiosError,
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
"server": "nodemon .\\examples\\server\\serverTest.mjs"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"/dist",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
|
@ -26,25 +26,38 @@
|
|||
"author": "",
|
||||
"license": "MulanPSL2",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.8",
|
||||
"@babel/preset-env": "^7.21.5",
|
||||
"@babel/preset-react": "^7.9.4",
|
||||
"@babel/preset-typescript": "^7.21.4",
|
||||
"@rollup/plugin-commonjs": "^19.0.0",
|
||||
"@types/jest": "^29.2.5",
|
||||
"@types/react": "^17.0.34",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"babel-jest": "^20.0.3",
|
||||
"babel-loader": "^9.1.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"core-js": "3.32.1",
|
||||
"cors": "^2.8.5",
|
||||
"eslint": "^8.31.0",
|
||||
"express": "^4.18.2",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"jest": "^29.3.1",
|
||||
"jest-environment-jsdom": "^29.4.1",
|
||||
"jsdom": "^22.0.0",
|
||||
"nodemon": "^2.0.22",
|
||||
"prettier": "^2.6.2",
|
||||
"rollup": "^3.20.2",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-typescript2": "^0.34.1",
|
||||
"ts-jest": "^29.0.4",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslib": "^2.5.0",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.81.0",
|
||||
"webpack-cli": "^5.0.2",
|
||||
"webpack-dev-server": "^4.13.3"
|
||||
|
|
|
@ -21,19 +21,16 @@ import { babel } from '@rollup/plugin-babel';
|
|||
|
||||
export default {
|
||||
input: './index.ts',
|
||||
output: [
|
||||
{
|
||||
file: 'dist/inulaRequest.js',
|
||||
format: 'umd',
|
||||
exports: 'named',
|
||||
name: 'inulaRequest',
|
||||
sourcemap: false,
|
||||
},
|
||||
{
|
||||
file: 'dist/inulaRequest.esm-browser.js',
|
||||
format: 'esm',
|
||||
},
|
||||
],
|
||||
output: [{
|
||||
file: 'dist/inulaRequest.js',
|
||||
format: 'umd',
|
||||
exports: 'named',
|
||||
name: 'inulaRequest',
|
||||
sourcemap: false,
|
||||
}, {
|
||||
file: 'dist/inulaRequest.esm-browser.js',
|
||||
format: 'esm',
|
||||
}],
|
||||
plugins: [
|
||||
resolve(),
|
||||
commonjs(),
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue