Compare commits

..

5 Commits

Author SHA1 Message Date
chaoling 96c4f0c337 feat: add inula-adapter/pinia 和 inula-adapter/vuex 2024-05-23 10:03:30 +08:00
chaoling 0f75e48f79 feat: update vue-adapter 2024-04-22 21:23:26 +08:00
chaoling b725b0d98c feat: add lifecycle 2024-04-19 15:21:17 +08:00
chaoling d7101ff5e3 feat: add useWatch 2024-04-18 15:55:20 +08:00
chaoling 05f0610c99 feat: add vue-reactive apis 2024-04-11 14:13:49 +08:00
365 changed files with 9251 additions and 15434 deletions

BIN
.DS_Store vendored

Binary file not shown.

1
.gitignore vendored
View File

@ -8,4 +8,3 @@ pnpm-lock.yaml
build build
/packages/inula-router/connectRouter /packages/inula-router/connectRouter
/packages/inula-router/router /packages/inula-router/router
.inula-max

View File

@ -9,9 +9,6 @@
"prettier": "prettier .prettierrc.js -w packages/**/*.{ts,tsx,js,jsx}", "prettier": "prettier .prettierrc.js -w packages/**/*.{ts,tsx,js,jsx}",
"build:inula": "pnpm -F openinula build", "build:inula": "pnpm -F openinula build",
"test:inula": "pnpm -F openinula test", "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-cli": "pnpm -F inula-cli build",
"build:inula-intl": "pnpm -F inula-intl build", "build:inula-intl": "pnpm -F inula-intl build",
"build:inula-request": "pnpm -F inula-request build", "build:inula-request": "pnpm -F inula-request build",
@ -25,48 +22,46 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.23.7", "@babel/core": "7.16.7",
"@babel/plugin-proposal-class-properties": "7.18.6", "@babel/plugin-proposal-class-properties": "7.16.7",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", "@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
"@babel/plugin-proposal-object-rest-spread": "7.20.7", "@babel/plugin-proposal-object-rest-spread": "7.16.7",
"@babel/plugin-proposal-optional-chaining": "7.21.0", "@babel/plugin-proposal-optional-chaining": "7.16.7",
"@babel/plugin-proposal-private-methods": "7.18.6", "@babel/plugin-proposal-private-methods": "7.16.7",
"@babel/plugin-proposal-private-property-in-object": "7.21.11", "@babel/plugin-proposal-private-property-in-object": "7.16.7",
"@babel/plugin-syntax-jsx": "7.23.3", "@babel/plugin-syntax-jsx": "7.16.7",
"@babel/plugin-transform-arrow-functions": "7.23.3", "@babel/plugin-transform-arrow-functions": "7.16.7",
"@babel/plugin-transform-block-scoped-functions": "7.23.3", "@babel/plugin-transform-block-scoped-functions": "7.16.7",
"@babel/plugin-transform-block-scoping": "7.23.4", "@babel/plugin-transform-block-scoping": "7.16.7",
"@babel/plugin-transform-classes": "7.23.8", "@babel/plugin-transform-classes": "7.16.7",
"@babel/plugin-transform-computed-properties": "7.23.3", "@babel/plugin-transform-computed-properties": "7.16.7",
"@babel/plugin-transform-destructuring": "7.23.3", "@babel/plugin-transform-destructuring": "7.16.7",
"@babel/plugin-transform-for-of": "7.23.6", "@babel/plugin-transform-for-of": "7.16.7",
"@babel/plugin-transform-literals": "7.23.3", "@babel/plugin-transform-literals": "7.16.7",
"@babel/plugin-transform-object-assign": "7.23.3", "@babel/plugin-transform-object-assign": "7.16.7",
"@babel/plugin-transform-object-super": "7.23.3", "@babel/plugin-transform-object-super": "7.16.7",
"@babel/plugin-transform-parameters": "7.23.3", "@babel/plugin-transform-parameters": "7.16.7",
"@babel/plugin-transform-react-jsx": "7.23.4", "@babel/plugin-transform-react-jsx": "7.16.7",
"@babel/plugin-transform-react-jsx-source": "^7.23.3", "@babel/plugin-transform-react-jsx-source": "^7.16.7",
"@babel/plugin-transform-runtime": "7.23.7", "@babel/plugin-transform-runtime": "7.16.7",
"@babel/plugin-transform-shorthand-properties": "7.23.3", "@babel/plugin-transform-shorthand-properties": "7.16.7",
"@babel/plugin-transform-spread": "7.23.3", "@babel/plugin-transform-spread": "7.16.7",
"@babel/plugin-transform-template-literals": "7.23.3", "@babel/plugin-transform-template-literals": "7.16.7",
"@babel/preset-env": "7.23.8", "@babel/preset-env": "7.16.7",
"@babel/preset-typescript": "7.23.3", "@babel/preset-typescript": "7.16.7",
"@babel/runtime": "7.23.8", "@babel/runtime": "7.16.7",
"@commitlint/cli": "^17.8.1", "@commitlint/cli": "^18.4.4",
"@commitlint/config-conventional": "^17.8.1", "@commitlint/config-conventional": "^18.4.4",
"@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^4.0.0", "@rollup/plugin-replace": "^4.0.0",
"@types/jest": "^29.5.11", "@types/jest": "^26.0.24",
"@types/node": "^17.0.18", "@types/node": "^17.0.18",
"@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/eslint-plugin": "4.8.0",
"@typescript-eslint/parser": "6.18.1", "@typescript-eslint/parser": "4.8.0",
"@babel/parser": "^7.24.7", "babel-jest": "^27.5.1",
"magic-string": "^0.30.10",
"babel-jest": "^29.7.0",
"ejs": "^3.1.8", "ejs": "^3.1.8",
"eslint": "^8.56.0", "eslint": "7.13.0",
"eslint-config-prettier": "^6.9.0", "eslint-config-prettier": "^6.9.0",
"eslint-plugin-jest": "^22.15.0", "eslint-plugin-jest": "^22.15.0",
"eslint-plugin-no-function-declare-after-return": "^1.0.0", "eslint-plugin-no-function-declare-after-return": "^1.0.0",
@ -77,13 +72,9 @@
"lint-staged": "^15.2.0", "lint-staged": "^15.2.0",
"openinula": "workspace:*", "openinula": "workspace:*",
"prettier": "^3.1.1", "prettier": "^3.1.1",
"rollup": "^2.79.1", "rollup": "^2.75.5",
"rollup-plugin-dts": "^6.1.0",
"rollup-plugin-execute": "^1.1.1", "rollup-plugin-execute": "^1.1.1",
"rollup-plugin-terser": "^7.0.2", "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" "typescript": "^4.9.5"
}, },
"engines": { "engines": {

View File

@ -32,8 +32,8 @@ const generatorType = fs
}); });
const runGenerator = async (templatePath, { name = '', cwd = process.cwd(), args = {} }) => { const runGenerator = async (templatePath, { name = '', cwd = process.cwd(), args = {} }) => {
let currentPath;
return new Promise(resolve => { return new Promise(resolve => {
let currentPath;
if (name) { if (name) {
mkdirp.sync(name); mkdirp.sync(name);
currentPath = path.join(cwd, name); currentPath = path.join(cwd, name);

View File

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

View File

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

View File

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

View File

@ -0,0 +1,101 @@
## pinia接口差异
1、不支createPinia的相关函数使用了是没有效果的
```js
it('not support createPinia', () => {
const pinia = createPinia(); // 接口可以调用,但是没有效果
const store = useStore(pinia); // 传入pinia也没有效果
expect(store.name).toBe('a');
expect(store.$state.name).toBe('a');
expect(pinia.state.value.main.name).toBe('a'); // 不能通过pinia.state.value.main获取store
});
```
2、因为不支持createPinia同样也不支持setActivePinia、getActivePinia()
3、不支持store.$patch
```js
it('can not be set with patch', () => {
const pinia = createPinia();
const store = useStore(pinia);
store.$patch({ name: 'a' }); // 不支持
// 改成
store.$state.name = 'a'; // 可以改成直接赋值
expect(store.name).toBe('a');
});
```
4、不支持store.$reset();
```ts
it('can not reset the state', () => {
const store = useStore();
store.name = 'Ed';
store.nested.n++;
store.$reset(); // 不支持
expect(store.$state).toEqual({
counter: 0,
name: 'Eduardo',
nested: {
n: 0,
},
});
});
```
5、不支持store.$dispose可以用store.$unsubscribe代替
```js
it('can not be disposed', () => {
const useStore = defineStore({
id: 'main',
state: () => ({ n: 0 }),
});
const store = useStore();
const spy = vi.fn();
store.$subscribe(spy, { flush: 'sync' });
store.$state.n++;
expect(spy).toHaveBeenCalledTimes(1);
expect(useStore()).toBe(store);
// store.$dispose();
// 改成
store.$unsubscribe(spy);
store.$state.n++;
expect(spy).toHaveBeenCalledTimes(1);
});
```
6、支持$subscribe不需要flush默认就是sync
```js
it('can be $unsubscribe', () => {
const useStore = defineStore({
id: 'main',
state: () => ({ n: 0 }),
});
const store = useStore();
const spy = vi.fn();
// store.$subscribe(spy, { flush: 'sync' });
// 不需要flush默认就是sync
store.$subscribe(spy, { flush: 'sync' });
store.$state.n++;
expect(spy).toHaveBeenCalledTimes(1);
expect(useStore()).toBe(store);
store.$unsubscribe(spy);
store.$state.n++;
expect(spy).toHaveBeenCalledTimes(1);
});
```

View File

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

View File

@ -0,0 +1,188 @@
## vuex接口差异
1、createStore接口中的state、mutations、actions、getters不能用相同名字属性
```js
it('dispatching actions, sync', () => {
const store = createStore({
state: {
a: 1
},
mutations: {
[TEST] (state, n) {
state.a += n
}
},
actions: {
[TEST] ({ commit }, n) {
commit(TEST, n)
}
}
})
store.dispatch(TEST, 2)
expect(store.state.a).toBe(3)
})
```
2、createStore接口不支持strict属性
```js
const store = createStore({
state: {
a: 1
},
mutations: {
[TEST] (state, n) {
state.a += n
}
},
actions: {
[TEST] ({ commit }, n) {
commit(TEST, n)
}
},
strict: true
})
```
3、store.registerModule不支持传入数组只支持一层的module注册
```js
it('dynamic module registration with namespace inheritance', () => {
const store = createStore({
modules: {
a: {
namespaced: true,
},
},
});
const actionSpy = vi.fn();
const mutationSpy = vi.fn();
store.registerModule(['a', 'b'], {
state: { value: 1 },
getters: { foo: state => state.value },
actions: { foo: actionSpy },
mutations: { foo: mutationSpy },
});
expect(store.state.a.b.value).toBe(1);
expect(store.getters['a/foo']).toBe(1);
store.dispatch('a/foo');
expect(actionSpy).toHaveBeenCalled();
store.commit('a/foo');
expect(mutationSpy).toHaveBeenCalled();
});
```
4、createStore不支持多层mudule注册
```js
it('module: mutation', function () {
const mutations = {
[TEST](state, n) {
state.a += n;
},
};
const store = createStore({
state: {
a: 1,
},
mutations,
modules: { // 第一层module支持
nested: {
state: {a: 2},
mutations,
modules: { // 第二层module不支持
one: {
state: {a: 3},
mutations,
},
nested: {
modules: { // 第三层module不支持
two: {
state: {a: 4},
mutations,
},
three: {
state: {a: 5},
mutations,
},
},
},
},
},
four: {
state: {a: 6},
mutations,
},
},
});
});
```
4、不支持store.replaceState
```js
const store = createStore({});
store.replaceState({ a: { foo: 'state' } });
```
5、store.registerModule不支持传入options参数
```js
store.registerModule(
'a',
{
namespaced: true,
getters: { foo: state => state.foo },
actions: { foo: actionSpy },
mutations: { foo: mutationSpy },
},
{ preserveState: true }
);
```
5、dispatch不支持传入options参数
```js
dispatch('foo', null, {root: true});
```
6、不支持action中的root属性action需要是个函数
```js
const store = createStore({
modules: {
a: {
namespaced: true,
actions: {
[TEST]: {
root: true,
handler() {
return 1;
},
},
},
},
}
});
```
7、createStore中不支持plugins
```js
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST](state, n) {
state.a += n;
},
},
actions: {
[TEST]: actionSpy,
},
plugins: [ // 不支持plugins
store => {
initState = store.state;
store.subscribe((mut, state) => {
expect(state).toBe(state);
mutations.push(mut);
});
store.subscribeAction(subscribeActionSpy);
},
],
});
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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. * openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the 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. * See the Mulan PSL v2 for more details.
*/ */
export function watch(stateVariable: any, listener: (state: any) => void) { export * from './pinia';
listener = listener.bind(null, stateVariable);
stateVariable.addListener(listener);
return () => {
stateVariable.removeListener(listener);
};
}

View File

@ -0,0 +1,208 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { createStore, StoreObj, vueReactive } from 'openinula';
import {
FilterAction,
FilterComputed,
FilterState,
StoreDefinition,
StoreSetup,
Store,
AnyFunction,
ActionType,
StoreToRefsReturn,
} from './types';
const { ref, isRef, toRef, isReactive, isReadonly } = vueReactive;
const storeMap = new Map<string, any>();
export function defineStore<
Id extends string,
S extends Record<string, unknown>,
A extends Record<string, AnyFunction>,
C extends Record<string, AnyFunction>,
>(definition: StoreDefinition<Id, S, A, C>): (pinia?: any) => Store<S, A, C>;
export function defineStore<
Id extends string,
S extends Record<string, unknown>,
A extends Record<string, AnyFunction>,
C extends Record<string, AnyFunction>,
>(id: Id, definition: Omit<StoreDefinition<Id, S, A, C>, 'id'>): (pinia?: any) => Store<S, A, C>;
export function defineStore<Id extends string, SS extends Record<any, unknown>>(
id: Id,
setup: StoreSetup<SS>
): (pinia?: any) => Store<FilterState<SS>, FilterAction<SS>, FilterComputed<SS>>;
export function defineStore(idOrDef: any, setupOrDef?: any) {
let id: string;
let definition: StoreDefinition | StoreSetup;
let isSetup = false;
if (typeof idOrDef === 'string') {
isSetup = typeof setupOrDef === 'function';
id = idOrDef;
definition = setupOrDef;
} else {
id = idOrDef.id;
definition = idOrDef;
}
if (isSetup) {
return defineSetupStore(id, definition as StoreSetup);
} else {
return defineOptionsStore(id, definition as StoreDefinition);
}
}
/**
* createStore实现中会给actions增加第一个参数storepinia不需要
* @param actions
*/
function enhanceActions(
actions?: ActionType<Record<string, AnyFunction>, Record<string, unknown>, Record<string, AnyFunction>>
) {
if (!actions) {
return {};
}
return Object.fromEntries(
Object.entries(actions).map(([key, value]) => {
return [
key,
function (this: StoreObj, state: Record<string, unknown>, ...args: any[]) {
return value.bind(this)(...args);
},
];
})
);
}
function defineOptionsStore(id: string, definition: StoreDefinition) {
const state = definition.state ? definition.state() : {};
const computed = definition.getters || {};
const actions = enhanceActions(definition.actions) || {};
return () => {
if (storeMap.has(id)) {
return storeMap.get(id)!();
}
const useStore = createStore({
id,
state,
actions,
computed,
});
storeMap.set(id, useStore);
return useStore();
};
}
function defineSetupStore<SS extends Record<string, unknown>>(id: string, storeSetup: StoreSetup<SS>) {
return () => {
const data = storeSetup();
if (!data) {
return {};
}
if (storeMap.has(id)) {
return storeMap.get(id)!();
}
const state: Record<string, unknown> = {};
const actions: Record<string, AnyFunction> = {};
const getters: Record<string, AnyFunction> = {};
for (const key in data) {
const prop = data[key];
if ((isRef(prop) && !isReadonly(prop)) || isReactive(prop)) {
// state
state[key] = prop;
} else if (typeof prop === 'function') {
// action
actions[key] = prop as AnyFunction;
} else if (isRef(prop) && isReadonly(prop)) {
// getters
getters[key] = (prop as any).fn as AnyFunction;
}
}
const useStore = createStore({
id,
state,
computed: getters,
actions: enhanceActions(actions),
});
storeMap.set(id, useStore);
return useStore();
};
}
export function mapStores<
S extends Record<string, unknown>,
A extends Record<string, AnyFunction>,
C extends Record<string, AnyFunction>,
>(...stores: (() => Store<S, A, C>)[]): { [key: string]: () => Store<S, A, C> } {
const result: { [key: string]: () => Store<S, A, C> } = {};
stores.forEach((store: () => Store<S, A, C>) => {
const expandedStore = store();
result[`${expandedStore.id}Store`] = () => expandedStore;
});
return result;
}
export function storeToRefs<
S extends Record<string, unknown>,
A extends Record<string, AnyFunction>,
C extends Record<string, AnyFunction>,
>(store: Store<S, A, C>): StoreToRefsReturn<S, C> {
const stateRefs = Object.fromEntries(
Object.entries(store.$s || {}).map(([key, value]) => {
return [key, ref(value)];
})
);
const getterRefs = Object.fromEntries(
Object.entries(store.$config.computed || {}).map(([key, value]) => {
const computeFn = (value as () => any).bind(store, store.$s);
return [key, toRef(computeFn)];
})
);
return { ...stateRefs, ...getterRefs } as StoreToRefsReturn<S, C>;
}
export function createPinia() {
console.warn(
`The pinia-adapter in Horizon does not support the createPinia interface. Please modify your code accordingly.`
);
const result = {
install: (app: any) => {},
use: (plugin: any) => result,
state: {},
};
return result;
}

View File

@ -0,0 +1,104 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
// import type {RefType, StoreObj, UnwrapRef, UserActions, UserComputedValues, ComputedImpl} from 'openinula';
import type { RefType, UnwrapRef, ComputedImpl } from 'openinula';
export type StoreSetup<R = Record<string, unknown>> = () => R;
export type AnyFunction = (...args: any[]) => any;
// defineStore init type
export interface StoreDefinition<
Id extends string = string,
S extends Record<string, unknown> = Record<string, unknown>,
A extends Record<string, AnyFunction> = Record<string, AnyFunction>,
C extends Record<string, AnyFunction> = Record<string, AnyFunction>,
> {
id?: Id;
state?: () => S;
actions?: ActionType<A, S, C>;
getters?: ComputedType<C, S>;
}
// defineStore return type
export type Store<
S extends Record<string, unknown>,
A extends Record<string, AnyFunction>,
C extends Record<string, AnyFunction>,
> = {
$s: S;
$state: S;
$a: ActionType<A, S, C>;
$c: ComputedType<C, S>;
$subscribe: (listener: Listener) => void;
$unsubscribe: (listener: Listener) => void;
} & { [K in keyof S]: S[K] } & { [K in keyof ActionType<A, S, C>]: ActionType<A, S, C>[K] } & {
[K in keyof ComputedType<C, S>]: ReturnType<ComputedType<C, S>[K]>;
};
export type ActionType<A, S, C> = A & ThisType<A & UnwrapRef<S> & WithGetters<C>>;
type ComputedType<C, S> = {
[K in keyof C]: AddFirstArg<C[K], S>;
} & ThisType<UnwrapRef<S> & WithGetters<C>>;
type AddFirstArg<T, S> = T extends (...args: infer A) => infer R
? (state: S, ...args: A) => R
: T extends () => infer R
? (state: S) => R
: T;
// In Getter function, make this.xx can refer to other getters
export type WithGetters<G> = {
readonly [k in keyof G]: G[k] extends (...args: any[]) => infer R ? R : UnwrapRef<G[k]>;
};
type Listener = (change: any) => void;
// Filter state properties
export type FilterState<T extends Record<string, unknown>> = {
[K in FilterStateProperties<T>]: UnwrapRef<T[K]>;
};
type FilterStateProperties<T extends Record<string, unknown>> = {
[K in keyof T]: T[K] extends ComputedImpl
? never
: T[K] extends RefType
? K
: T[K] extends Record<any, unknown> // Reactive类型
? K
: never;
}[keyof T];
// Filter action properties
export type FilterAction<T extends Record<string, unknown>> = {
[K in FilterFunctionProperties<T>]: T[K] extends AnyFunction ? T[K] : never;
};
type FilterFunctionProperties<T extends Record<string, unknown>> = {
[K in keyof T]: T[K] extends AnyFunction ? K : never;
}[keyof T];
// Filter computed properties
export type FilterComputed<T extends Record<string, unknown>> = {
[K in FilterComputedProperties<T>]: T[K] extends ComputedImpl<infer T> ? (T extends AnyFunction ? T : never) : never;
};
type FilterComputedProperties<T extends Record<string, unknown>> = {
[K in keyof T]: T[K] extends ComputedImpl ? K : never;
}[keyof T];
export type StoreToRefsReturn<S extends Record<string, unknown>, C extends Record<string, AnyFunction>> = {
[K in keyof S]: RefType<S[K]>;
} & {
[K in keyof ComputedType<C, S>]: Readonly<RefType<ReturnType<ComputedType<C, S>[K]>>>;
};

View File

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

View File

@ -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?.();
};
}, []);
}

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
export type FN = () => void;

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
export * from './vuex';

View File

@ -0,0 +1,104 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
export type AnyFunction = (...args: any[]) => any;
export interface VuexStoreOptions<
State extends Record<string, unknown> = Record<string, unknown>,
Mutations extends Record<string, AnyFunction> = Record<string, AnyFunction>,
Actions extends Record<string, AnyFunction> = Record<string, AnyFunction>,
Getters extends Record<string, AnyFunction> = Record<string, AnyFunction>,
RootState extends Record<string, unknown> = Record<string, unknown>,
RootGetters extends Record<string, AnyFunction> = Record<string, AnyFunction>,
Modules extends Record<string, Record<string, unknown>> = Record<string, Record<string, unknown>>,
> {
namespaced?: boolean;
state?: State | (() => State);
mutations?: MutationsType<Mutations, State>;
actions?: ActionsType<Actions, State, Getters, RootState, RootGetters>;
getters?: GettersType<State, Getters, State, Getters>;
modules?: {
[k in keyof Modules]: VuexStoreOptions<Modules[k]>;
};
}
type MutationsType<Mutations, State> = {
[K in keyof Mutations]: AddFirstArg<Mutations[K], State>;
};
type ActionsType<Actions, State, Getters, RootState, RootGetters> = {
[K in keyof Actions]: AddFirstArg<
Actions[K],
{
commit: CommitType;
dispatch: DispatchType;
state: State;
getters: Getters;
rootState: RootState;
rootGetters: RootGetters;
}
>;
};
type AddFirstArg<T, S> = T extends (arg1: any, ...args: infer A) => infer R
? (state: S, ...args: A) => R
: T extends () => infer R
? (state: S) => R
: T;
type GettersType<State, Getters, RootState, RootGetters> = {
[K in keyof Getters]: AddArgs<Getters[K], [State, Getters, RootState, RootGetters]>;
};
type AddArgs<T, Args extends any[]> = T extends (...args: infer A) => infer R
? (...args: [...Args, ...A]) => R
: T extends () => infer R
? (...args: Args) => R
: T;
export type CommitType = (
type: string | (Record<string, unknown> & { type: string }),
payload?: any,
options?: Record<string, unknown>,
moduleName?: string
) => void;
export type DispatchType = (
type: string | (Record<string, unknown> & { type: string }),
payload?: any,
options?: Record<string, unknown>,
moduleName?: string
) => any;
export type VuexStore<
State extends Record<string, unknown> = Record<string, unknown>,
Getters extends Record<string, AnyFunction> = Record<string, AnyFunction>,
Modules extends Record<string, Record<string, unknown>> = Record<string, Record<string, unknown>>,
> = {
state: State & {
[K in keyof Modules]: Modules[K] extends { state: infer ModuleState } ? ModuleState : Modules[K];
};
getters: {
[K in keyof Getters]: ReturnType<Getters[K]>;
};
commit: CommitType;
dispatch: DispatchType;
subscribe: AnyFunction;
subscribeAction: AnyFunction;
watch: (fn: (state: State, getters: Getters) => void, cb: AnyFunction) => void;
registerModule: (moduleName: string, module: VuexStoreOptions) => void;
unregisterModule: (moduleName: string) => void;
hasModule: (moduleName: string) => boolean;
};

View File

@ -0,0 +1,315 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { createStore as createStoreX, StoreObj, vueReactive } from 'openinula';
import { VuexStore, VuexStoreOptions } from './types';
import { AnyFunction } from '../pinia/types';
const { watch } = vueReactive;
const MUTATION_PREFIX = 'm_';
const GETTER_PREFIX = 'g_';
type GettersMap<T extends StoreObj = StoreObj> = {
[K in keyof T['$c']]: ReturnType<T['$c'][K]>;
};
export function createStore<
State extends Record<string, unknown> = Record<string, unknown>,
Mutations extends Record<string, AnyFunction> = Record<string, AnyFunction>,
Actions extends Record<string, AnyFunction> = Record<string, AnyFunction>,
Getters extends Record<string, AnyFunction> = Record<string, AnyFunction>,
RootState extends Record<string, unknown> = Record<string, unknown>,
RootGetters extends Record<string, AnyFunction> = Record<string, AnyFunction>,
Modules extends Record<string, Record<string, unknown>> = Record<string, Record<string, unknown>>,
>(
options: VuexStoreOptions<State, Mutations, Actions, Getters, RootState, RootGetters, Modules>
): VuexStore<State, Getters, Modules> {
const modules = options.modules || {};
const _modules: Record<string, { storeX: StoreObj; namespaced: boolean }> = {};
const _getters: GettersMap = {};
const vuexStore: VuexStore = {
state: new Proxy(
{},
{
get: (_, key) => {
if (key in _modules) {
return _modules[key as string].storeX;
} else {
return rootStoreX[key as string];
}
},
}
),
getters: new Proxy(
{},
{
get: (_, key) => {
if (typeof key === 'string') {
// 如果key包含/说明是访问模块的getters进行split
if (key.includes('/')) {
const [moduleName, getterKey] = key.split('/');
return _modules[moduleName].storeX[`${GETTER_PREFIX}${getterKey}`];
} else {
return _getters[`${GETTER_PREFIX}${key}`];
}
}
},
}
),
commit: (_type, _payload, _options, moduleName) => {
const { type, payload, options } = prepareTypeParams(_type, _payload, _options);
// 如果options.root为true调用根store的action
if (options?.root) {
return rootStoreX[`${MUTATION_PREFIX}${type}`](payload);
}
// 包含/说明是访问模块的mutation
if (type.includes('/')) {
const [moduleName, key] = type.split('/');
return _modules[moduleName].storeX[`${MUTATION_PREFIX}${key}`](payload);
}
if (moduleName != undefined) {
// dispatch到指定的module
return _modules[moduleName].storeX[`${MUTATION_PREFIX}${type}`](payload);
}
// 调用所有非namespaced的modules的mutation
Object.values(_modules).forEach(module => {
if (!module.namespaced) {
const mutation = module.storeX[`${MUTATION_PREFIX}${type}`];
if (typeof mutation === 'function') {
mutation(payload);
}
}
});
// 调用storeX对象上的方法
if (rootStoreX[`${MUTATION_PREFIX}${type}`]) {
rootStoreX[`${MUTATION_PREFIX}${type}`](payload);
}
},
dispatch: (_type, _payload, _options, moduleName) => {
const { type, payload, options } = prepareTypeParams(_type, _payload, _options);
// 如果options.root为true调用根store的action
if (options?.root) {
return rootStoreX[type](payload);
}
// 包含/说明是访问模块的action
if (type.includes('/')) {
const [moduleName, key] = type.split('/');
return _modules[moduleName].storeX[key](payload);
}
if (moduleName != undefined) {
// dispatch到指定的module
return _modules[moduleName].storeX[type](payload);
}
// 把每个action的返回值合并起来支持then链式调用
const results: any[] = [];
// 调用所有非namespaced的modules的action
Object.values(_modules).forEach(module => {
if (!module.namespaced) {
const action = module.storeX[type];
if (typeof action === 'function') {
results.push(action(payload));
}
}
});
// 调用storeX对象上的方法
if (typeof rootStoreX[type] === 'function') {
results.push(rootStoreX[type](payload));
}
// 返回一个Promise内容是results支持then链式调用
return Promise.all(results);
},
subscribe(fn) {
return rootStoreX.$subscribe(fn);
},
subscribeAction(fn) {
return rootStoreX.$subscribe(fn);
},
watch(fn, cb) {
watch(() => fn(vuexStore.state, vuexStore.getters), cb);
},
// 动态注册模块
registerModule(key, module) {
_modules[key] = { storeX: _createStoreX(key, module, vuexStore, rootStoreX), namespaced: !!module.namespaced };
collectGetters(_modules[key].storeX, _getters);
},
// 动态注销模块
unregisterModule(key) {
deleteGetters(_modules[key].storeX, _getters);
delete _modules[key];
},
hasModule(path) {
return path in _modules;
},
};
const rootStoreX = _createStoreX(undefined, options as VuexStoreOptions, vuexStore);
collectGetters(rootStoreX, _getters);
// 递归创建子模块
for (const [moduleName, moduleOptions] of Object.entries(modules)) {
_modules[moduleName] = {
storeX: _createStoreX(moduleName, moduleOptions as VuexStoreOptions, vuexStore, rootStoreX),
namespaced: !!(moduleOptions as VuexStoreOptions).namespaced,
};
collectGetters(_modules[moduleName].storeX, _getters);
}
return vuexStore as VuexStore<State, Getters, Modules>;
}
export function prepareTypeParams(
type: string | (Record<string, unknown> & { type: string }),
payload?: any,
options?: Record<string, unknown>
) {
if (typeof type === 'object' && type.type) {
options = payload;
payload = type;
type = type.type;
}
return { type, payload, options } as {
type: string;
payload: any;
options: Record<string, unknown>;
};
}
function _createStoreX(
moduleName: string | undefined,
options: VuexStoreOptions,
store: VuexStore,
rootStoreX?: any
): StoreObj {
const { mutations = {}, actions = {}, getters = {} } = options;
const state = typeof options.state === 'function' ? options.state() : options.state;
const storeX: StoreObj = createStoreX({
id: moduleName,
state: state,
actions: {
// 给mutations的key增加一个前缀避免和actions的key冲突
...Object.fromEntries(
Object.entries(mutations).map(([key, mutation]) => {
return [`${MUTATION_PREFIX}${key}`, mutation];
})
),
// 重新定义action的方法绑定this修改第一参数
...Object.fromEntries(
Object.entries(actions).map(([key, action]) => [
key,
function (this: StoreObj, state: Record<string, unknown>, payload) {
rootStoreX = rootStoreX || storeX;
const argFirst = {
...store,
// 覆盖commit方法多传一个参数moduleName
commit: (
type: string | (Record<string, unknown> & { type: string }),
payload?: any,
options?: Record<string, unknown>
) => {
store.commit(type, payload, options, moduleName);
},
// 覆盖dispatch方法多传一个参数moduleName
dispatch: (
type: string | (Record<string, unknown> & { type: string }),
payload?: any,
options?: Record<string, unknown>
) => {
return store.dispatch(type, payload, options, moduleName);
},
state: storeX.$state,
rootState: store.state,
getter: store.getters,
rootGetters: moduleGettersProxy(rootStoreX),
};
return action.call(storeX, argFirst, payload);
},
])
),
},
computed: {
...Object.fromEntries(
Object.entries(getters).map(([key, getter]) => {
return [
// 给getters的key增加一个前缀避免和actions, mutations的key冲突
`${GETTER_PREFIX}${key}`,
// 重新定义getter的方法绑定this修改参数: state, getters, rootState, rootGetters
function (state: Record<string, unknown>) {
rootStoreX = rootStoreX || storeX;
return getter.call(
storeX,
storeX.$state,
store.getters,
rootStoreX.$state,
moduleGettersProxy(rootStoreX)
);
},
];
})
),
},
})();
return storeX;
}
function collectGetters(storeX: StoreObj, gettersMap: GettersMap): void {
Object.keys(storeX.$config.computed).forEach(type => {
Object.defineProperty(gettersMap, type, {
get: () => storeX.$c[type],
configurable: true,
});
});
}
function deleteGetters(storeX: StoreObj, gettersMap: GettersMap): void {
Object.keys(storeX.$config.computed).forEach(type => {
// 删除Object.defineProperty定义的属性
Object.defineProperty(gettersMap, type, {
value: undefined,
writable: true,
enumerable: true,
configurable: true,
});
delete gettersMap[type];
});
}
function moduleGettersProxy(storeX: StoreObj) {
return new Proxy(
{},
{
get: (_, key) => {
return storeX[`${GETTER_PREFIX}${key as string}`];
},
}
);
}

View File

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

View File

@ -0,0 +1,97 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { defineStore } from '../../src/pinia/pinia';
let id = 0;
function createStore() {
return defineStore({
id: String(id++),
state: () => ({
name: 'Eduardo',
}),
getters: {
upperCaseName(store) {
return store.name.toUpperCase();
},
doubleName(): string {
return this.upperCaseName;
},
composed(): string {
return this.upperCaseName + ': ok';
},
arrowUpper: state => {
// @ts-expect-error
state.nope;
return state.name.toUpperCase();
},
},
actions: {
o() {
this.arrowUpper.toUpperCase();
this.o().toUpperCase();
return 'a string';
},
},
});
}
describe('pinia getters', () => {
let useStore = createStore();
let useB;
let useA;
beforeEach(() => {
useStore = createStore();
useB = defineStore({
id: 'B',
state: () => ({ b: 'b' }),
});
});
it('adds getters to the store', () => {
const store = useStore();
expect(store.upperCaseName).toBe('EDUARDO');
// @ts-expect-error
store.nope;
store.name = 'Ed';
expect(store.upperCaseName).toBe('ED');
});
it('updates the value', () => {
const store = useStore();
store.name = 'Ed';
expect(store.upperCaseName).toBe('ED');
});
it('can use other getters', () => {
const store = useStore();
expect(store.composed).toBe('EDUARDO: ok');
store.name = 'Ed';
expect(store.composed).toBe('ED: ok');
});
it('keeps getters reactive when hydrating', () => {
const store = useStore();
store.name = 'Jack';
expect(store.name).toBe('Jack');
expect(store.upperCaseName).toBe('JACK');
store.name = 'Ed';
expect(store.upperCaseName).toBe('ED');
});
});

View File

@ -0,0 +1,138 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { beforeEach, describe, it, vi, expect } from 'vitest';
import { createPinia, defineStore } from '../../src/pinia/pinia';
import { vueReactive } from 'openinula';
const { watch, computed, ref, reactive } = vueReactive;
let id = 0;
function createStore() {
return defineStore(String(id++), {
state: () => ({
name: 'Eduardo',
counter: 0,
nested: { n: 0 },
}),
actions: {
increment(state, amount) {
this.counter += amount;
},
},
getters: {
upperCased() {
return this.name.toUpperCase();
},
},
});
}
describe('pinia state', () => {
let useStore = createStore();
beforeEach(() => {
useStore = createStore();
});
it('can directly access state at the store level', () => {
const store = useStore();
expect(store.name).toBe('Eduardo');
store.name = 'Ed';
expect(store.name).toBe('Ed');
});
it('state is reactive', () => {
const store = useStore();
const upperCased = computed(() => store.name.toUpperCase());
expect(upperCased.value).toBe('EDUARDO');
store.name = 'Ed';
expect(upperCased.value).toBe('ED');
});
it('can be set on store', () => {
const pinia = createPinia();
const store = useStore(pinia);
store.name = 'a';
expect(store.name).toBe('a');
expect(store.$state.name).toBe('a');
});
it('can be set on store.$state', () => {
const pinia = createPinia();
const store = useStore(pinia);
store.$state.name = 'a';
expect(store.name).toBe('a');
expect(store.$state.name).toBe('a');
});
it('can be nested set on store', () => {
const pinia = createPinia();
const store = useStore(pinia);
store.nested.n = 3;
expect(store.nested.n).toBe(3);
expect(store.$state.nested.n).toBe(3);
});
it('can be nested set on store.$state', () => {
const pinia = createPinia();
const store = useStore(pinia);
store.$state.nested.n = 3;
expect(store.nested.n).toBe(3);
expect(store.$state.nested.n).toBe(3);
});
it('state can be watched', async () => {
const store = useStore();
const spy = vi.fn();
watch(() => store.name, spy);
expect(spy).not.toHaveBeenCalled();
store.name = 'Ed';
expect(spy).toHaveBeenCalledTimes(1);
});
it('state can be watched when a ref is given', async () => {
const store = useStore();
const spy = vi.fn();
watch(() => store.name, spy);
expect(spy).not.toHaveBeenCalled();
const nameRef = ref('Ed');
// @ts-expect-error
store.$state.name = nameRef;
expect(spy).toHaveBeenCalledTimes(1);
});
it('can be given a ref', () => {
const pinia = createPinia();
const store = useStore(pinia);
// @ts-expect-error
store.$state.name = ref('Ed');
expect(store.name).toBe('Ed');
expect(store.$state.name).toBe('Ed');
store.name = 'Other';
expect(store.name).toBe('Other');
expect(store.$state.name).toBe('Other');
});
});

View File

@ -0,0 +1,120 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { beforeEach, describe, it, vi, expect } from 'vitest';
import { defineStore } from '../../src/pinia/pinia';
import { vueReactive } from 'openinula';
const { watch } = vueReactive;
let id = 0;
function createStore() {
return defineStore({
id: String(id++),
state: () => ({
a: true,
nested: {
foo: 'foo',
a: { b: 'string' },
},
}),
});
}
describe('pinia state', () => {
let useStore = createStore();
beforeEach(() => {
useStore = createStore();
});
it('reuses a store', () => {
const useStore = defineStore({ id: String(id++) });
expect(useStore()).toBe(useStore());
});
it('works with id as first argument', () => {
const useStore = defineStore(String(id++), {
state: () => ({
a: true,
nested: {
foo: 'foo',
a: { b: 'string' },
},
}),
});
expect(useStore()).toBe(useStore());
const useStoreEmpty = defineStore(String(id++), {});
expect(useStoreEmpty()).toBe(useStoreEmpty());
});
it('sets the initial state', () => {
const store = useStore();
expect(store.$state).toEqual({
a: true,
nested: {
foo: 'foo',
a: { b: 'string' },
},
});
});
it.skip('can replace its state', () => {
const store = useStore();
const spy = vi.fn();
watch(() => store.a, spy);
expect(store.a).toBe(true);
expect(spy).toHaveBeenCalledTimes(0);
store.$state = {
a: false,
nested: {
foo: 'bar',
a: {
b: 'hey',
},
},
};
expect(spy).toHaveBeenCalledTimes(1);
expect(store.$state).toEqual({
a: false,
nested: {
foo: 'bar',
a: { b: 'hey' },
},
});
});
it('can be $unsubscribe', () => {
const useStore = defineStore({
id: 'main',
state: () => ({ n: 0 }),
});
const store = useStore();
const spy = vi.fn();
store.$subscribe(spy);
store.$state.n++;
expect(spy).toHaveBeenCalledTimes(1);
expect(useStore()).toBe(store);
store.$unsubscribe(spy);
store.$state.n++;
expect(spy).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,152 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { beforeEach, describe, it, vi, expect } from 'vitest';
import { defineStore } from '../../src/pinia/pinia';
import { vueReactive } from 'openinula';
const { ref, watch, computed } = vueReactive;
function expectType<T>(_value: T): void {}
describe('store with setup syntax', () => {
function mainFn() {
const name = ref('Eduardo');
const counter = ref(0);
function increment(amount = 1) {
counter.value += amount;
}
const double = computed(() => counter.value * 2);
return { name, counter, increment, double };
}
let id = 0;
function createStore() {
return defineStore(String(id++), mainFn);
}
let useStore = createStore();
beforeEach(() => {
useStore = createStore();
});
it('should extract the $state', () => {
const store = useStore();
expectType<{ name: string; counter: number }>(store.$state);
expect(store.$state).toEqual({ name: 'Eduardo', counter: 0 });
expect(store.name).toBe('Eduardo');
expect(store.counter).toBe(0);
expect(store.double).toBe(0);
store.increment();
expect(store.counter).toBe(1);
expect(store.double).toBe(2);
expect(store.$state).toEqual({ name: 'Eduardo', counter: 1 });
expect(store.$state).not.toHaveProperty('double');
expect(store.$state).not.toHaveProperty('increment');
});
it('can store a function', () => {
const store = defineStore(String(id++), () => {
const fn = ref(() => {});
function action() {}
return { fn, action };
})();
expectType<{ fn: () => void }>(store.$state);
expect(store.$state).toEqual({ fn: expect.any(Function) });
expect(store.fn).toEqual(expect.any(Function));
store.action();
});
it('can directly access state at the store level', () => {
const store = useStore();
expect(store.name).toBe('Eduardo');
store.name = 'Ed';
expect(store.name).toBe('Ed');
});
it('state is reactive', () => {
const store = useStore();
const upperCased = computed(() => store.name.toUpperCase());
expect(upperCased.value).toBe('EDUARDO');
store.name = 'Ed';
expect(upperCased.value).toBe('ED');
});
it('state can be watched', async () => {
const store = useStore();
const spy = vi.fn();
watch(() => store.name, spy);
expect(spy).not.toHaveBeenCalled();
store.name = 'Ed';
expect(spy).toHaveBeenCalledTimes(1);
});
it('state refs can be watched', async () => {
const store = useStore();
const spy = vi.fn();
watch(() => store.name, spy);
expect(spy).not.toHaveBeenCalled();
const nameRef = ref('Ed');
store.name = nameRef;
expect(spy).toHaveBeenCalledTimes(1);
});
it('unwraps refs', () => {
const name = ref('Eduardo');
const counter = ref(0);
const double = computed(() => {
return counter.value * 2;
});
// const pinia = createPinia();
// setActivePinia(pinia);
const useStore = defineStore({
id: String(id++),
state: () => ({
name,
counter,
double,
}),
});
const store = useStore();
expect(store.name).toBe('Eduardo');
expect(store.$state.name).toBe('Eduardo');
expect(store.$state).toEqual({
name: 'Eduardo',
counter: 0,
double: 0,
});
name.value = 'Ed';
expect(store.name).toBe('Ed');
expect(store.$state.name).toBe('Ed');
store.$state.name = 'Edu';
expect(store.name).toBe('Edu');
// store.$patch({ counter: 2 });
store.counter = 2;
expect(store.counter).toBe(2);
expect(counter.value).toBe(2);
});
});

View File

@ -0,0 +1,141 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { beforeEach, describe, it, vi, expect } from 'vitest';
import { defineStore, storeToRefs } from '../../src/pinia/pinia';
import { vueReactive } from 'openinula';
const { ref, computed, reactive } = vueReactive;
let id = 0;
describe('storeToRefs', () => {
beforeEach(() => {});
function objectOfRefs<O extends Record<any, any>>(o: O) {
return Object.keys(o).reduce((newO, key) => {
// @ts-expect-error: we only need to match
newO[key] = expect.objectContaining({ value: o[key] });
return newO;
}, {});
}
it('empty state', () => {
expect(storeToRefs(defineStore(String(id++), {})())).toEqual({});
expect(storeToRefs(defineStore({ id: String(id++) })())).toEqual({});
});
it('plain values', () => {
const store = defineStore(String(id++), {
state: () => ({ a: null as null | undefined, b: false, c: 1, d: 'd' }),
})();
const { a, b, c, d } = storeToRefs(store);
expect(a.value).toBe(null);
expect(b.value).toBe(false);
expect(c.value).toBe(1);
expect(d.value).toBe('d');
a.value = undefined;
expect(a.value).toBe(undefined);
b.value = true;
expect(b.value).toBe(true);
c.value = 2;
expect(c.value).toBe(2);
d.value = 'e';
expect(d.value).toBe('e');
});
it('setup store', () => {
const store = defineStore(String(id++), () => {
return {
a: ref<null | undefined>(null),
b: ref(false),
c: ref(1),
d: ref('d'),
r: reactive({ n: 1 }),
};
})();
const { a, b, c, d, r } = storeToRefs(store);
expect(a.value).toBe(null);
expect(b.value).toBe(false);
expect(c.value).toBe(1);
expect(d.value).toBe('d');
expect(r.value).toEqual({ n: 1 });
a.value = undefined;
expect(a.value).toBe(undefined);
b.value = true;
expect(b.value).toBe(true);
c.value = 2;
expect(c.value).toBe(2);
d.value = 'e';
expect(d.value).toBe('e');
r.value.n++;
expect(r.value).toEqual({ n: 2 });
expect(store.r).toEqual({ n: 2 });
store.r.n++;
expect(r.value).toEqual({ n: 3 });
expect(store.r).toEqual({ n: 3 });
});
it('empty getters', () => {
expect(
storeToRefs(
defineStore(String(id++), {
state: () => ({ n: 0 }),
})()
)
).toEqual(objectOfRefs({ n: 0 }));
expect(
storeToRefs(
defineStore(String(id++), () => {
return { n: ref(0) };
})()
)
).toEqual(objectOfRefs({ n: 0 }));
});
it('contains getters', () => {
const refs = storeToRefs(
defineStore(String(id++), {
state: () => ({ n: 1 }),
getters: {
double: state => state.n * 2,
},
})()
);
expect(refs).toEqual(objectOfRefs({ n: 1, double: 2 }));
const setupRefs = storeToRefs(
defineStore(String(id++), () => {
const n = ref(1);
const double = computed(() => n.value * 2);
return { n, double };
})()
);
expect(setupRefs).toEqual(objectOfRefs({ n: 1, double: 2 }));
});
});

View File

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

View File

@ -0,0 +1,480 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { createStore } from '../../src/vuex/vuex';
const TEST = 'TEST';
describe('Modules', () => {
it('dynamic module registration', () => {
const store = createStore({
modules: {
foo: {
state: { bar: 1 },
mutations: { inc: state => state.bar++ },
actions: { inc: ({ commit }) => commit('inc') },
getters: { fooBar: state => state.bar },
},
one: {
state: { a: 0 },
mutations: {
aaa(state, n) {
state.a += n;
},
},
},
},
});
store.registerModule('hi', {
state: { a: 1 },
mutations: { inc: state => state.a++ },
actions: { incHi: ({ commit }) => commit('inc') },
getters: { ga: state => state.a },
});
// expect(store._mutations.inc.length).toBe(2);
expect(store.state.hi.a).toBe(1);
expect(store.getters.ga).toBe(1);
// assert initial modules work as expected after dynamic registration
expect(store.state.foo.bar).toBe(1);
expect(store.getters.fooBar).toBe(1);
// test dispatching actions defined in dynamic module
store.dispatch('incHi');
expect(store.state.hi.a).toBe(2);
expect(store.getters.ga).toBe(2);
expect(store.state.foo.bar).toBe(1);
expect(store.getters.fooBar).toBe(1);
// unregister
store.unregisterModule('hi');
expect(store.state.hi).toBeUndefined();
expect(store.getters.ga).toBeUndefined();
// assert initial modules still work as expected after unregister
store.dispatch('inc');
expect(store.state.foo.bar).toBe(2);
expect(store.getters.fooBar).toBe(2);
});
it('dynamic module registration with namespace inheritance', () => {
const store = createStore({
modules: {
a: {
namespaced: true,
},
},
});
const actionSpy = vi.fn();
const mutationSpy = vi.fn();
store.registerModule('b', {
state: { value: 1 },
getters: { foo: state => state.value },
actions: { foo: actionSpy },
mutations: { foo: mutationSpy },
});
expect(store.state.b.value).toBe(1);
expect(store.getters['foo']).toBe(1);
store.dispatch('foo');
expect(actionSpy).toHaveBeenCalled();
store.commit('foo');
expect(mutationSpy).toHaveBeenCalled();
});
it('dynamic module existance test', () => {
const store = createStore({});
store.registerModule('bonjour', {});
expect(store.hasModule('bonjour')).toBe(true);
store.unregisterModule('bonjour');
expect(store.hasModule('bonjour')).toBe(false);
});
it('should keep getters when component gets destroyed', async () => {
const store = createStore({});
const spy = vi.fn();
const moduleA = {
namespaced: true,
state: () => ({ value: 1 }),
getters: {
getState(state) {
spy();
return state.value;
},
},
mutations: {
increment: state => {
state.value++;
},
},
};
store.registerModule('moduleA', moduleA);
expect(store.getters['moduleA/getState']).toBe(1);
expect(spy).toHaveBeenCalledTimes(1);
store.commit('moduleA/increment');
expect(store.getters['moduleA/getState']).toBe(2);
expect(spy).toHaveBeenCalledTimes(2);
});
it('should not fire an unrelated watcher', () => {
const spy = vi.fn();
const store = createStore({
modules: {
a: {
state: { value: 1 },
},
b: {},
},
});
store.watch(state => state.a, spy);
store.registerModule('c', {
state: { value: 2 },
});
expect(spy).not.toHaveBeenCalled();
});
it('state as function (multiple module in same store)', () => {
const store = createStore({
modules: {
one: {
state: { a: 0 },
mutations: {
[TEST](state, n) {
state.a += n;
},
},
},
two: {
state() {
return { a: 0 };
},
mutations: {
[TEST](state, n) {
state.a += n;
},
},
},
},
});
expect(store.state.one.a).toBe(0);
expect(store.state.two.a).toBe(0);
store.commit(TEST, 1);
expect(store.state.one.a).toBe(1);
expect(store.state.two.a).toBe(1);
});
it('state as function (same module in multiple stores)', () => {
const storeA = createStore({
modules: {
foo: {
state() {
return { a: 0 };
},
mutations: {
[TEST](state, n) {
state.a += n;
},
},
},
},
});
const storeB = createStore({
modules: {
bar: {
state() {
return { a: 0 };
},
mutations: {
[TEST](state, n) {
state.a += n;
},
},
},
},
});
expect(storeA.state.foo.a).toBe(0);
expect(storeB.state.bar.a).toBe(0);
storeA.commit(TEST, 1);
expect(storeA.state.foo.a).toBe(1);
expect(storeB.state.bar.a).toBe(0);
storeB.commit(TEST, 2);
expect(storeA.state.foo.a).toBe(1);
expect(storeB.state.bar.a).toBe(2);
});
it('module: mutation', function () {
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST](state, n) {
state.a += n;
},
},
modules: {
nested: {
state: { a: 2 },
mutations: {
[TEST](state, n) {
state.a += n;
},
},
},
four: {
state: { a: 6 },
mutations: {
[TEST](state, n) {
state.a += n;
},
},
},
},
});
store.commit(TEST, 1);
expect(store.state.a).toBe(2);
expect(store.state.nested.a).toBe(3);
expect(store.state.four.a).toBe(7);
});
it('module: action', function () {
let calls = 0;
const store = createStore({
state: {
a: 1,
},
actions: {
[TEST]({ state, rootState }) {
calls++;
expect(state.a).toBe(1);
expect(rootState).toBe(store.state);
},
},
modules: {
nested: {
state: { a: 2 },
actions: {
[TEST]({ state, rootState }) {
calls++;
expect(state.a).toBe(2);
expect(rootState).toBe(store.state);
},
},
},
four: {
state: { a: 6 },
actions: {
[TEST]({ state, rootState }) {
calls++;
expect(state.a).toBe(6);
expect(rootState).toBe(store.state);
},
},
},
},
});
store.dispatch(TEST);
expect(calls).toBe(3);
});
it('module: getters', function () {
const store = createStore({
state: {
a: 1,
},
getters: {
constant: () => 0,
[`getter1`]: (state, getters, rootState) => {
expect(getters.constant).toBe(0);
expect(rootState.a).toBe(store.state.a);
return state.a;
},
},
modules: {
nested: {
state: { a: 2 },
getters: {
[`getter2`]: (state, getters, rootState) => {
expect(getters.constant).toBe(0);
expect(rootState.a).toBe(store.state.a);
return state.a;
},
},
},
four: {
state: { a: 6 },
getters: {
[`getter6`]: (state, getters, rootState) => {
expect(getters.constant).toBe(0);
expect(rootState.a).toBe(store.state.a);
return state.a;
},
},
},
},
});
[1, 2, 6].forEach(n => {
expect(store.getters[`getter${n}`]).toBe(n);
});
});
it('module: namespace', () => {
const actionSpy = vi.fn();
const mutationSpy = vi.fn();
const store = createStore({
modules: {
a: {
namespaced: true,
state: {
a: 1,
},
getters: {
b: () => 2,
},
actions: {
[TEST]: actionSpy,
},
mutations: {
[TEST]: mutationSpy,
},
},
},
});
expect(store.state.a.a).toBe(1);
expect(store.getters['a/b']).toBe(2);
store.dispatch('a/' + TEST);
expect(actionSpy).toHaveBeenCalled();
store.commit('a/' + TEST);
expect(mutationSpy).toHaveBeenCalled();
});
it('module: getters are namespaced in namespaced module', () => {
const store = createStore({
state: { value: 'root' },
getters: {
foo: state => state.value,
},
modules: {
a: {
namespaced: true,
state: { value: 'module' },
getters: {
foo: state => {
return state.value;
},
bar: (state, getters) => {
return getters.foo;
},
baz: (state, getters, rootState, rootGetters) => rootGetters.foo,
},
},
},
});
expect(store.getters['a/foo']).toBe('module');
expect(store.getters['a/bar']).toBe('module');
expect(store.getters['a/baz']).toBe('root');
});
it('module: action context is namespaced in namespaced module', () => {
const rootActionSpy = vi.fn();
const rootMutationSpy = vi.fn();
const moduleActionSpy = vi.fn();
const moduleMutationSpy = vi.fn();
const store = createStore({
state: { value: 'root' },
getters: { foo: state => state.value },
actions: { foo: rootActionSpy },
mutations: { foo: rootMutationSpy },
modules: {
a: {
namespaced: true,
state: { value: 'module' },
getters: { foo: state => state.value },
actions: {
foo: moduleActionSpy,
test({ dispatch, commit, getters, rootGetters }) {
expect(getters.foo).toBe('module');
expect(rootGetters.foo).toBe('root');
dispatch('foo');
expect(moduleActionSpy).toHaveBeenCalledTimes(1);
dispatch('foo', null, { root: true });
expect(rootActionSpy).toHaveBeenCalledTimes(1);
commit('foo');
expect(moduleMutationSpy).toHaveBeenCalledTimes(1);
commit('foo', null, { root: true });
expect(rootMutationSpy).toHaveBeenCalledTimes(1);
},
},
mutations: { foo: moduleMutationSpy },
},
},
});
store.dispatch('a/test');
});
it('dispatching multiple actions in different modules', () => {
const store = createStore({
modules: {
a: {
actions: {
[TEST]() {
return 1;
},
},
},
b: {
actions: {
[TEST]() {
return new Promise(r => r(2));
},
},
},
},
});
store.dispatch(TEST).then(res => {
expect(res[0]).toBe(1);
expect(res[1]).toBe(2);
});
});
});

View File

@ -0,0 +1,335 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { describe, it, expect, vi } from 'vitest';
import { createStore } from '../../src/vuex/vuex';
const TEST_M = 'TEST_M';
const TEST_A = 'TEST_A';
describe('vuex Store', () => {
it('committing mutations', () => {
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST_M](state, n) {
state.a += n;
},
},
});
store.commit(TEST_M, 2);
expect(store.state.a).toBe(3);
});
it('committing with object style', () => {
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST_M](state, payload) {
state.a += payload.amount;
},
},
});
store.commit({
type: TEST_M,
amount: 2,
});
expect(store.state.a).toBe(3);
});
it('dispatching actions, sync', () => {
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST_M](state, n) {
state.a += n;
},
},
actions: {
[TEST_A]({ commit }, n) {
commit(TEST_M, n);
},
},
});
store.dispatch(TEST_A, 2);
expect(store.state.a).toBe(3);
});
it('dispatching with object style', () => {
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST_M](state, n) {
state.a += n;
},
},
actions: {
[TEST_A]({ commit }, payload) {
commit(TEST_M, payload.amount);
},
},
});
store.dispatch({
type: TEST_A,
amount: 2,
});
expect(store.state.a).toBe(3);
});
it('dispatching actions, with returned Promise', () => {
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST_M](state, n) {
state.a += n;
},
},
actions: {
[TEST_A]({ commit }, n) {
return new Promise(resolve => {
setTimeout(() => {
commit(TEST_M, n);
resolve('');
}, 0);
});
},
},
});
expect(store.state.a).toBe(1);
store.dispatch(TEST_A, 2).then(() => {
expect(store.state.a).toBe(3);
});
});
it('composing actions with async/await', () => {
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST_M](state, n) {
state.a += n;
},
},
actions: {
[TEST_A]({ commit }, n) {
return new Promise(resolve => {
setTimeout(() => {
commit(TEST_M, n);
resolve('');
}, 0);
});
},
two: async ({ commit, dispatch }, n) => {
await dispatch(TEST_A, 1);
expect(store.state.a).toBe(2);
commit(TEST_M, n);
},
},
});
expect(store.state.a).toBe(1);
store.dispatch('two', 3).then(() => {
expect(store.state.a).toBe(5);
});
});
it('detecting action Promise errors', () => {
const store = createStore({
actions: {
[TEST_A]() {
return new Promise((resolve, reject) => {
reject('no');
});
},
},
});
const thenSpy = vi.fn();
store
.dispatch(TEST_A)
.then(thenSpy)
.catch((err: string) => {
expect(thenSpy).not.toHaveBeenCalled();
expect(err).toBe('no');
});
});
it('getters', () => {
const store = createStore({
state: {
a: 0,
},
getters: {
state: state => (state.a > 0 ? 'hasAny' : 'none'),
},
mutations: {
[TEST_M](state, n) {
state.a += n;
},
},
actions: {
check({ getters }, value) {
// check for exposing getters into actions
expect(getters.state).toBe(value);
},
},
});
expect(store.getters.state).toBe('none');
store.dispatch('check', 'none');
store.commit(TEST_M, 1);
expect(store.getters.state).toBe('hasAny');
store.dispatch('check', 'hasAny');
});
it('should accept state as function', () => {
const store = createStore({
state: () => ({
a: 1,
}),
mutations: {
[TEST_M](state, n) {
state.a += n;
},
},
});
expect(store.state.a).toBe(1);
store.commit(TEST_M, 2);
expect(store.state.a).toBe(3);
});
it('subscribe: should handle subscriptions / unsubscriptions', () => {
const subscribeSpy = vi.fn();
const secondSubscribeSpy = vi.fn();
const testPayload = 2;
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST_M](state) {
state.a++;
},
},
});
const unsubscribe = store.subscribe(subscribeSpy);
store.subscribe(secondSubscribeSpy);
store.commit(TEST_M, testPayload);
unsubscribe();
store.commit(TEST_M, testPayload);
expect(subscribeSpy).toHaveBeenCalledTimes(1);
expect(secondSubscribeSpy).toHaveBeenCalledTimes(2);
});
it('subscribe: should handle subscriptions with synchronous unsubscriptions', () => {
const subscribeSpy = vi.fn();
const testPayload = 2;
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST_M](state) {
state.a++;
},
},
});
const unsubscribe = store.subscribe(() => unsubscribe());
store.subscribe(subscribeSpy);
store.commit(TEST_M, testPayload);
expect(subscribeSpy).toHaveBeenCalledTimes(1);
});
it('subscribeAction: should handle subscriptions with synchronous unsubscriptions', () => {
const subscribeSpy = vi.fn();
const testPayload = 2;
const store = createStore({
state: {
a: 1,
},
actions: {
[TEST_A]({ state }) {
state.a++;
},
},
});
const unsubscribe = store.subscribeAction(() => unsubscribe());
store.subscribeAction(subscribeSpy);
store.dispatch(TEST_A, testPayload);
expect(subscribeSpy).toHaveBeenCalledTimes(1);
});
it('watch: with resetting vm', () => {
const store = createStore({
state: {
count: 0,
},
mutations: {
[TEST_M]: state => state.count++,
},
});
const spy = vi.fn();
store.watch(state => state.count, spy);
store.commit(TEST_M);
expect(store.state.count).toBe(1);
expect(spy).toHaveBeenCalledTimes(1);
});
it("watch: getter function has access to store's getters object", () => {
const store = createStore({
state: {
count: 0,
},
mutations: {
[TEST_M]: state => state.count++,
},
getters: {
getCount: state => state.count,
},
});
const getter = function getter(state: any) {
return state.count;
};
const spy = vi.spyOn({ getter }, 'getter');
const spyCb = vi.fn();
store.watch(spy as any, spyCb);
store.commit(TEST_M);
expect(store.state.count).toBe(1);
expect(spy).toHaveBeenCalledWith(store.state, store.getters);
});
});

View File

@ -0,0 +1,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, // jsts.
"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"]
}

View File

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

View File

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

View File

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

View File

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

View File

@ -14,3 +14,4 @@
*/ */
declare module 'crequire'; declare module 'crequire';

View File

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

View File

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

View File

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

View File

@ -7,12 +7,12 @@ function deleteFolder(filePath) {
if (fs.lstatSync(filePath).isDirectory()) { if (fs.lstatSync(filePath).isDirectory()) {
const files = fs.readdirSync(filePath); const files = fs.readdirSync(filePath);
files.forEach(file => { files.forEach(file => {
const nextFilePath = path.join(filePath, file); const nectFilePath = path.join(filePath, file);
const states = fs.lstatSync(nextFilePath); const states = fs.lstatSync(nectFilePath);
if (states.isDirectory()) { if (states.isDirectory()) {
deleteFolder(nextFilePath); deleteFolder(nectFilePath);
} else { } else {
fs.unlinkSync(nextFilePath); fs.unlinkSync(nectFilePath);
} }
}); });
fs.rmdirSync(filePath); fs.rmdirSync(filePath);
@ -31,12 +31,12 @@ export function cleanUp(folders) {
return { return {
name: 'clean-up', name: 'clean-up',
buildEnd() { buildEnd() {
folders.forEach(f => deleteFolder(f)); folders.forEach(folder => deleteFolder(folder));
}, },
}; };
} }
function buildTypeConfig() { function builderTypeConfig() {
return { return {
input: './build/@types/index.d.ts', input: './build/@types/index.d.ts',
output: { output: {
@ -47,4 +47,4 @@ function buildTypeConfig() {
}; };
} }
export default [buildTypeConfig()]; export default [builderTypeConfig()];

View File

@ -13,7 +13,7 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import { useState } from 'openinula'; import Inula, { useState } from 'openinula';
import { IntlProvider } from '../index'; import { IntlProvider } from '../index';
import zh from './locale/zh'; import zh from './locale/zh';
import en from './locale/en'; import en from './locale/en';
@ -32,29 +32,23 @@ const App = () => {
const message = locale === 'zh' ? zh : en; const message = locale === 'zh' ? zh : en;
return ( return (
<> <IntlProvider locale={locale} messages={locale === 'zh' ? zh : en}>
<IntlProvider locale={locale} messages={locale === 'zh' ? zh : en}> <header>Inula-Intl API Test Demo</header>
<header>Inula-Intl API Test Demo</header>
<div className="container"> <div className="container">
<Example1 /> <Example1 />
<Example2 /> <Example2 />
<Example3 locale={locale} setLocale={setLocale} /> <Example3 locale={locale} setLocale={setLocale} />
</div> </div>
<div className="container">
{/*<Example4 locale={locale} messages={message} />*/}
<Example5 />
</div>
<div className="button">
<button onClick={handleChange}></button>
</div>
</IntlProvider>
<div className="container"> <div className="container">
<Example4 locale={locale} messages={message} /> <Example4 locale={locale} messages={message} />
</div> <Example5 />
<div className="container">
<Example6 locale={{ locale }} messages={message} /> <Example6 locale={{ locale }} messages={message} />
</div> </div>
</> <div className="button">
<button onClick={handleChange}></button>
</div>
</IntlProvider>
); );
}; };

View File

@ -13,16 +13,16 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula from 'openinula';
import { useIntl } from '../../index'; import { useIntl } from '../../index';
const Example1 = () => { const Example1 = () => {
const i18n = useIntl(); const { i18n } = useIntl();
return ( return (
<div className="card"> <div className="card">
<h2>useIntl方式测试Demo</h2> <h2>useIntl方式测试Demo</h2>
<pre>{i18n.formatMessage({ id: 'text1' })}</pre> <pre>{i18n.formatMessage({ id: 'text1' })}</pre>
<pre>{i18n.$t({ id: 'text1' })}</pre>
</div> </div>
); );
}; };

View File

@ -12,6 +12,7 @@
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula from 'openinula';
import { FormattedMessage } from '../../index'; import { FormattedMessage } from '../../index';
const Example2 = () => { const Example2 = () => {
@ -21,9 +22,6 @@ const Example2 = () => {
<pre> <pre>
<FormattedMessage id="text2" /> <FormattedMessage id="text2" />
</pre> </pre>
<pre>
<FormattedMessage id="text5" values={{ testComponent1: <b>123</b>, testComponent2: <b>456</b> }} />
</pre>
</div> </div>
); );
}; };

View File

@ -13,6 +13,7 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula from 'openinula';
import { FormattedMessage } from '../../index'; import { FormattedMessage } from '../../index';
const Example3 = props => { const Example3 = props => {

View File

@ -13,6 +13,7 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula from 'openinula';
import { createIntl } from '../../index'; import { createIntl } from '../../index';
const Example4 = props => { const Example4 = props => {

View File

@ -13,16 +13,23 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula, { Component } from 'openinula';
import { injectIntl } from '../../index'; import { injectIntl } from '../../index';
const Example5 = ({ intl }) => { class Example5 extends Component<any, any, any> {
// 使用intl.formatMessage来获取国际化消息 public constructor(props: any, context) {
console.log(intl + '------------intl-------------'); super(props, context);
return ( }
<div className="card">
<h2>injectIntl方式测试Demo</h2> render() {
<pre>{intl.formatMessage({ id: 'text4' })}</pre> const { intl } = this.props as any;
</div> return (
); <div className="card">
}; <h2>injectIntl方式测试Demo</h2>
<pre>{intl.formatMessage({ id: 'text4' })}</pre>
</div>
);
}
}
export default injectIntl(Example5); export default injectIntl(Example5);

View File

@ -13,6 +13,7 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula from 'openinula';
import { createIntl, createIntlCache, RawIntlProvider } from '../../index'; import { createIntl, createIntlCache, RawIntlProvider } from '../../index';
import Example6Child from './Example6Child'; import Example6Child from './Example6Child';
@ -20,7 +21,7 @@ const Example6 = (props: any) => {
const { locale, messages } = props; const { locale, messages } = props;
const cache = createIntlCache(); const cache = createIntlCache();
const i18n = createIntl({ locale: locale, messages: messages }, cache); let i18n = createIntl({ locale: locale, messages: messages }, cache);
return ( return (
<RawIntlProvider value={i18n}> <RawIntlProvider value={i18n}>

View File

@ -15,7 +15,7 @@
import { useIntl } from '../../index'; import { useIntl } from '../../index';
const Example6Child = () => { const Example6Child = (props: any) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
return ( return (

View File

@ -12,7 +12,7 @@
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import Inula from 'openinula'; import * as Inula from 'openinula';
import App from './App'; import App from './App';
function render() { function render() {

View File

@ -19,5 +19,4 @@ export default {
text2: 'Welcome to the Inula-Intl component!', text2: 'Welcome to the Inula-Intl component!',
text3: 'Welcome to the Inula-Intl component!', text3: 'Welcome to the Inula-Intl component!',
text4: 'Welcome to the Inula-Intl component!', text4: 'Welcome to the Inula-Intl component!',
text5: 'Render a component {testComponent1} {testComponent2}!',
}; };

View File

@ -18,5 +18,4 @@ export default {
text2: '欢迎使用国际化组件!', text2: '欢迎使用国际化组件!',
text3: '欢迎使用国际化组件!', text3: '欢迎使用国际化组件!',
text4: '欢迎使用国际化组件!', text4: '欢迎使用国际化组件!',
text5: '渲染一个组件 {testComponent1} {testComponent2}!',
}; };

View File

@ -22,7 +22,7 @@ import I18nProvider from './src/core/components/I18nProvider';
import injectIntl, { I18nContext, InjectProvider } from './src/core/components/InjectI18n'; import injectIntl, { I18nContext, InjectProvider } from './src/core/components/InjectI18n';
import useI18n from './src/core/hook/useI18n'; import useI18n from './src/core/hook/useI18n';
import createI18n from './src/core/createI18n'; import createI18n from './src/core/createI18n';
import { MessageDescriptor } from './src/types/interfaces'; import { InjectedIntl, MessageDescriptor } from './src/types/interfaces';
// 函数API // 函数API
export { export {
I18n, I18n,
@ -36,7 +36,7 @@ export {
// 组件 // 组件
export { export {
FormattedMessage, FormattedMessage,
I18nContext as IntlContext, I18nContext,
I18nProvider as IntlProvider, I18nProvider as IntlProvider,
injectIntl as injectIntl, injectIntl as injectIntl,
InjectProvider as RawIntlProvider, 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 { export function defineMessage<T>(msg: T): T {
return msg; return msg;
} }
export interface InjectedIntlProps {
intl: InjectedIntl;
}

View File

@ -13,7 +13,7 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
export default { module.exports = {
coverageDirectory: 'coverage', coverageDirectory: 'coverage',
resetModules: true, resetModules: true,
preset: 'ts-jest/presets/js-with-ts', preset: 'ts-jest/presets/js-with-ts',
@ -30,10 +30,8 @@ export default {
globals: { globals: {
'ts-jest': { 'ts-jest': {
tsconfig: 'tsconfig.json', tsconfig: 'tsconfig.json',
diagnostics: false,
}, },
}, },
testPathIgnorePatterns: ['\\\\node_modules\\\\'],
testEnvironment: 'jsdom', testEnvironment: 'jsdom',
}; };

View File

@ -3,13 +3,13 @@
"version": "0.0.5", "version": "0.0.5",
"description": "", "description": "",
"main": "build/intl.umd.js", "main": "build/intl.umd.js",
"type": "module", "type": "commonjs",
"types": "build/@types/index.d.ts", "types": "build/@types/index.d.ts",
"scripts": { "scripts": {
"demo-serve": "webpack serve --mode=development", "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", "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" "test-c": "jest --coverage"
}, },
"repository": { "repository": {
@ -17,7 +17,8 @@
"url": "" "url": ""
}, },
"files": [ "files": [
"/build" "build",
"README.md"
], ],
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -26,23 +27,35 @@
"openinula": ">=0.1.1" "openinula": ">=0.1.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.21.3",
"@babel/preset-env": "^7.16.7",
"@babel/preset-react": "^7.9.4", "@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-typescript": "^11.0.0",
"rollup-plugin-dts": "^6.1.0",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@types/node": "^16.18.27",
"@types/react": "18.0.25", "@types/react": "18.0.25",
"babel": "^6.23.0",
"babel-jest": "^29.5.0",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.2",
"html-webpack-plugin": "^5.5.1", "html-webpack-plugin": "^5.5.1",
"jest": "29.3.1",
"jest-environment-jsdom": "^29.5.0", "jest-environment-jsdom": "^29.5.0",
"jsdom": "^21.1.1", "jsdom": "^21.1.1",
"react": "18.2.0", "prettier": "^2.8.7",
"react-dom": "18.2.0", "rollup": "^2.0.0",
"rollup-plugin-livereload": "^2.0.5", "rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-serve": "^1.1.0", "rollup-plugin-serve": "^1.1.0",
"rollup-plugin-visualizer": "^5.10.0", "rollup-plugin-terser": "^5.3.0",
"ts-node": "10.9.1",
"tslib": "^2.6.1", "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-cli": "^5.1.4",
"webpack-dev-server": "^4.13.3" "webpack-dev-server": "^4.13.3"
} }

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,6 @@ import babel from '@rollup/plugin-babel';
import nodeResolve from '@rollup/plugin-node-resolve'; import nodeResolve from '@rollup/plugin-node-resolve';
import typescript from '@rollup/plugin-typescript'; import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser'; import { terser } from 'rollup-plugin-terser';
import { visualizer } from 'rollup-plugin-visualizer';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@ -30,55 +29,34 @@ const output = path.join(__dirname, '/build');
const extensions = ['.js', '.ts', '.tsx']; const extensions = ['.js', '.ts', '.tsx'];
const BuildConfig = mode => { export default {
const prod = mode.startsWith('prod'); input: entry,
const outputList = [ output: [
{ {
file: path.join(output, `cjs/intl.${prod ? 'min.' : ''}js`), file: path.resolve(output, 'intl.umd.js'),
sourcemap: 'true',
format: 'cjs',
globals: {
openinula: 'Inula',
},
},
{
file: path.join(output, `umd/intl.${prod ? 'min.' : ''}js`),
name: 'InulaI18n', name: 'InulaI18n',
sourcemap: 'true',
format: 'umd', format: 'umd',
globals: {
openinula: 'Inula',
},
}, },
]; {
if (!prod) { file: path.resolve(output, 'intl.esm-browser.js'),
outputList.push({
file: path.join(output, 'esm/intl.js'),
sourcemap: 'true',
format: 'esm', format: 'esm',
}); }
} ],
return { plugins: [
input: entry, nodeResolve({
output: outputList, extensions,
plugins: [ modulesOnly: true,
nodeResolve({ }),
extensions, babel({
modulesOnly: true, exclude: 'node_modules/**',
}), configFile: path.join(__dirname, '/babel.config.js'),
babel({ extensions,
exclude: 'node_modules/**', }),
configFile: path.join(__dirname, '/.babelrc'), typescript({
extensions, tsconfig: 'tsconfig.json',
babelHelpers: 'runtime', include: ['./**/*.ts', './**/*.tsx'],
}), }),
typescript({ terser(),
tsconfig: 'tsconfig.json', ],
include: ['./**/*.ts', './**/*.tsx'], external: ['openinula', 'react', 'react-dom'],
}),
terser(),
],
external: ['openinula', 'react', 'react-dom'],
};
}; };
export default [BuildConfig('dev'), BuildConfig('prod')];

View File

@ -18,13 +18,8 @@
* \\x[a-fA-F0-9]{2} \x0A * \\x[a-fA-F0-9]{2} \x0A
* [nrtf'"] 匹配常见的转义字符:\n换行符、\r回车符、\t制表符、\f换页符、\' \" * [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 UNICODE_REG = /\\(?: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 VERTICAL_LINE: string = '|';
export const UNICODE_FLAG: string = 'u';
export const STATE_GROUP_START_INDEX: number = 1;
// Inula 需要被保留静态常量 // Inula 需要被保留静态常量
export const INULA_STATICS = { export const INULA_STATICS = {
childContextTypes: true, childContextTypes: true,
@ -81,22 +76,3 @@ export const INULA_MEMO_STATICS = {
// 默认复数规则 // 默认复数规则
export const DEFAULT_PLURAL_KEYS = ['zero', 'one', 'two', 'few', 'many', 'other']; 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',
];

View File

@ -18,48 +18,30 @@ import DateTimeFormatter from '../format/fomatters/DateTimeFormatter';
import NumberFormatter from '../format/fomatters/NumberFormatter'; import NumberFormatter from '../format/fomatters/NumberFormatter';
import { getFormatMessage } from '../format/getFormatMessage'; import { getFormatMessage } from '../format/getFormatMessage';
import { I18nCache, I18nProps, MessageDescriptor, MessageOptions } from '../types/interfaces'; import { I18nCache, I18nProps, MessageDescriptor, MessageOptions } from '../types/interfaces';
import { import { Locale, Locales, Messages, AllLocaleConfig, AllMessages, LocaleConfig, Error, Events } from '../types/types';
Locale,
Locales,
Messages,
AllLocaleConfig,
AllMessages,
LocaleConfig,
Error,
Events,
InulaNode,
} from '../types/types';
import creatI18nCache from '../format/cache/cache'; import creatI18nCache from '../format/cache/cache';
import { isValidElement } from 'openinula';
export class I18n extends EventDispatcher<Events> { export class I18n extends EventDispatcher<Events> {
public locale: Locale; public locale: Locale;
public locales: Locales; public locales: Locales;
public defaultLocale?: Locale;
public timeZone?: string;
private allMessages: AllMessages;
private readonly _localeConfig: AllLocaleConfig; private readonly _localeConfig: AllLocaleConfig;
public readonly onError?: Error; private readonly allMessages: AllMessages;
public readonly error?: Error;
public readonly cache?: I18nCache; public readonly cache?: I18nCache;
constructor(props: I18nProps) { constructor(props: I18nProps) {
super(); super();
this.defaultLocale = 'en'; this.locale = 'en';
this.locale = this.defaultLocale;
this.locales = this.locale || ''; this.locales = this.locale || '';
this.allMessages = {}; this.allMessages = {};
this._localeConfig = {}; this._localeConfig = {};
this.onError = props.onError; this.error = props.error;
this.timeZone = '';
this.loadMessage(props.messages); this.loadMessage(props.messages);
if (props.localeConfig) { if (props.localeConfig) {
this.loadLocaleConfig(props.localeConfig); this.loadLocaleConfig(props.localeConfig);
} }
if (props.messages) {
this.changeMessage(props.messages);
}
if (props.locale || props.locales) { if (props.locale || props.locales) {
this.changeLanguage(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 // 加载messages
loadMessage(localeOrMessages: Locale | AllMessages | undefined, messages?: Messages) { loadMessage(localeOrMessages: Locale | AllMessages | undefined, messages?: Messages) {
if (messages) { if (messages) {
@ -141,21 +118,9 @@ export class I18n extends EventDispatcher<Events> {
formatMessage( formatMessage(
id: MessageDescriptor | string, id: MessageDescriptor | string,
values: Record<string, unknown> | undefined = {}, values: Record<string, unknown> | undefined = {},
{ messages, context, formatOptions }: MessageOptions = {} { message, context, formatOptions }: MessageOptions = {}
) { ) {
// 在多次渲染时保证存储component不丢失 return getFormatMessage(this, id, values, { message, context, formatOptions });
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!);
} }
formatDate(value: string | Date, formatOptions?: Intl.DateTimeFormatOptions): string { formatDate(value: string | Date, formatOptions?: Intl.DateTimeFormatOptions): string {

View File

@ -12,7 +12,7 @@
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details. * 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 { FormattedMessageProps } from '../../types/interfaces';
import useI18n from '../hook/useI18n'; import useI18n from '../hook/useI18n';
@ -22,17 +22,28 @@ import useI18n from '../hook/useI18n';
* @constructor * @constructor
*/ */
function FormattedMessage(props: FormattedMessageProps) { function FormattedMessage(props: FormattedMessageProps) {
const { formatMessage } = useI18n(); const { i18n } = useI18n();
const { id, values, messages, formatOptions, context, tagName: TagName = Fragment, children, comment }: any = props; const {
id,
values,
messages,
formatOptions,
context,
tagName: TagName = Fragment,
children,
comment,
useMemorize,
}: any = props;
const formatMessageOptions = { const formatMessageOptions = {
comment, comment,
messages, messages,
context, context,
useMemorize,
formatOptions, formatOptions,
}; };
const formattedMessage = formatMessage(id, values, formatMessageOptions); let formattedMessage = i18n.formatMessage(id, values, formatMessageOptions);
if (typeof children === 'function') { if (typeof children === 'function') {
const childNodes = Array.isArray(formattedMessage) ? formattedMessage : [formattedMessage]; const childNodes = Array.isArray(formattedMessage) ? formattedMessage : [formattedMessage];

View File

@ -12,10 +12,10 @@
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details. * 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 { InjectProvider } from './InjectI18n';
import I18n, { createI18nInstance } from '../I18n'; 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 * @constructor
*/ */
const I18nProvider = (props: I18nProviderProps) => { const I18nProvider = (props: I18nProviderProps) => {
const { locale, messages, children, i18n } = props; const { locale, messages, children } = props;
const i18nInstance = const i18n = useMemo(() => {
i18n || return createI18nInstance({
useMemo(() => { locale: locale,
return createI18nInstance({ messages: messages,
locale: locale, });
messages: messages, }, [locale, messages]);
});
}, [locale, messages]);
// 使用useRef保存上次的locale值 // 使用useRef保存上次的locale值
const localeRef = useRef<string | undefined>(i18nInstance.locale); const localeRef = useRef<string | undefined>(i18n.locale);
const localeMessage = useRef<string | Messages | AllMessages>(i18nInstance.messages);
const [context, setContext] = useState<I18n>(i18nInstance); const [context, setContext] = useState<I18n>(i18n);
useEffect(() => { useEffect(() => {
const handleChange = () => { const handleChange = () => {
if (localeRef.current !== i18nInstance.locale || localeMessage.current !== i18nInstance.messages) { if (localeRef.current !== i18n.locale) {
localeRef.current = i18nInstance.locale; localeRef.current = i18n.locale;
localeMessage.current = i18nInstance.messages; setContext(i18n);
setContext(i18nInstance);
} }
}; };
const removeListener = i18nInstance.on('change', handleChange); let removeListener = i18n.on('change', handleChange);
// 手动触发一次 handleChange以确保 context 的正确性 // 手动触发一次 handleChange以确保 context 的正确性
handleChange(); handleChange();
@ -56,7 +53,7 @@ const I18nProvider = (props: I18nProviderProps) => {
return () => { return () => {
removeListener(); removeListener();
}; };
}, [i18nInstance]); }, [i18n]);
// 提供一个Provider组件 // 提供一个Provider组件
return <InjectProvider value={context}>{children}</InjectProvider>; return <InjectProvider value={context}>{children}</InjectProvider>;

View File

@ -31,16 +31,13 @@ export const InjectProvider = Provider;
function injectI18n(Component, options?: InjectOptions): any { function injectI18n(Component, options?: InjectOptions): any {
const { const {
isUsingForwardRef = false, // 默认不使用 isUsingForwardRef = false, // 默认不使用
ensureContext = false,
} = options || {}; } = options || {};
// 定义一个名为 WrappedI18n 的函数组件,接收传入组件的 props 和 forwardedRef返回传入组件并注入 i18n // 定义一个名为 WrappedI18n 的函数组件,接收传入组件的 props 和 forwardedRef返回传入组件并注入 i18n
const WrappedI18n = props => ( const WrappedI18n = props => (
<Consumer> <Consumer>
{context => { {context => {
if (ensureContext) { isVariantI18n(context);
isVariantI18n(context);
}
const i18nProps = { const i18nProps = {
intl: context, intl: context,

View File

@ -13,29 +13,20 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import { configProps, I18nCache } from '../types/interfaces'; import { configProps, I18nCache } from '../types/interfaces';
import { createI18nInstance } from './I18n'; import I18n, { createI18nInstance } from './I18n';
import creatI18nCache from '../format/cache/cache'; import creatI18nCache from '../format/cache/cache';
import { IntlType } from '../types/types';
/** /**
* createI18n hook函数i8n实例 * 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 { locale, defaultLocale, messages } = config;
const i18n = createI18nInstance({ return createI18nInstance({
locale: locale || defaultLocale || 'en', locale: locale || defaultLocale || 'zh',
messages: messages, messages: messages,
cache: cache ?? creatI18nCache(), 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; export default createI18n;

View File

@ -12,7 +12,7 @@
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import { useContext, useMemo } from 'openinula'; import Inula, { useContext } from 'openinula';
import utils from '../../utils/utils'; import utils from '../../utils/utils';
import { I18nContext } from '../components/InjectI18n'; import { I18nContext } from '../components/InjectI18n';
import I18n from '../I18n'; import I18n from '../I18n';
@ -23,22 +23,15 @@ import { IntlType } from '../../types/types';
* 使 useI18n 便 * 使 useI18n 便
*/ */
function useI18n(): IntlType { function useI18n(): IntlType {
const i18n = useContext<I18n>(I18nContext); const i18nContext = useContext<I18n>(I18nContext);
utils.isVariantI18n(i18n); utils.isVariantI18n(i18nContext);
return useMemo(() => { const i18n = i18nContext;
return { return {
i18n: i18n, i18n: i18n,
locale: i18n.locale, formatMessage: i18n.formatMessage.bind(i18n),
messages: i18n.messages, formatNumber: i18n.formatNumber.bind(i18n),
defaultLocale: i18n.defaultLocale, formatDate: i18n.formatDate.bind(i18n),
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]);
} }
export default useI18n; export default useI18n;

View File

@ -16,7 +16,7 @@
import { CompiledMessage, Locale, LocaleConfig, Locales } from '../types/types'; import { CompiledMessage, Locale, LocaleConfig, Locales } from '../types/types';
import generateFormatters from './generateFormatters'; import generateFormatters from './generateFormatters';
import { FormatOptions, I18nCache } from '../types/interfaces'; 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 localeConfig: Record<string, any>;
private readonly cache: I18nCache; private readonly cache: I18nCache;
constructor( constructor(compiledMessage, locale, locales, localeConfig, cache?) {
compiledMessage: CompiledMessage,
locale: Locale,
locales: Locales,
localeConfig: LocaleConfig,
cache?: I18nCache
) {
this.compiledMessage = compiledMessage; this.compiledMessage = compiledMessage;
this.locale = locale; this.locale = locale;
this.locales = locales; this.locales = locales;
this.localeConfig = localeConfig; this.localeConfig = localeConfig;
this.cache = cache ?? creatI18nCache(); this.cache = cache ?? createIntlCache;
} }
/** /**
@ -59,7 +53,7 @@ class Translation {
const value = values[name]; const value = values[name];
const formatter = formatters[type](value, format); const formatter = formatters[type](value, format);
let message: any; let message;
if (typeof formatter === 'function') { if (typeof formatter === 'function') {
message = formatter(textFormatter); // 递归调用 message = formatter(textFormatter); // 递归调用
} else { } else {
@ -74,7 +68,8 @@ class Translation {
const textFormatter = createTextFormatter(this.locale, this.locales, values, formatOptions, this.localeConfig); const textFormatter = createTextFormatter(this.locale, this.locales, values, formatOptions, this.localeConfig);
// 通过递归方法formatCore进行格式化处理 // 通过递归方法formatCore进行格式化处理
return this.formatMessage(this.compiledMessage, textFormatter); // 返回要格式化的结果 const result = this.formatMessage(this.compiledMessage, textFormatter);
return result; // 返回要格式化的结果
} }
formatMessage(compiledMessage: CompiledMessage, textFormatter: (...args: any[]) => any) { formatMessage(compiledMessage: CompiledMessage, textFormatter: (...args: any[]) => any) {

View File

@ -17,7 +17,7 @@ import utils from '../../utils/utils';
import NumberFormatter from './NumberFormatter'; import NumberFormatter from './NumberFormatter';
import { Locale, Locales } from '../../types/types'; import { Locale, Locales } from '../../types/types';
import { I18nCache } from '../../types/interfaces'; 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 message: any;
private readonly cache: I18nCache; 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.locale = locale;
this.locales = locales; this.locales = locales;
this.value = value; this.value = value;
this.message = message; this.message = message;
this.cache = cache ?? creatI18nCache(); this.cache = cache ?? createIntlCache();
} }
// 将 message中的“#”替换为指定数字value并返回新的字符串或者字符串数组 // 将 message中的“#”替换为指定数字value并返回新的字符串或者字符串数组

View File

@ -14,7 +14,7 @@
*/ */
import utils from '../../utils/utils'; import utils from '../../utils/utils';
import {Locale, SelectPool} from '../../types/types'; import { Locale } from '../../types/types';
import { I18nCache } from '../../types/interfaces'; import { I18nCache } from '../../types/interfaces';
/** /**
@ -26,12 +26,12 @@ class SelectFormatter {
private readonly locale: Locale; private readonly locale: Locale;
private readonly cache: I18nCache; private readonly cache: I18nCache;
constructor(locale: Locale, cache: I18nCache) { constructor(locale, cache) {
this.locale = locale; this.locale = locale;
this.cache = cache; this.cache = cache;
} }
getRule(value: SelectPool, rules: any) { getRule(value, rules) {
if (this.cache.select) { if (this.cache.select) {
// 创建key用于唯一标识 // 创建key用于唯一标识
const cacheKey = utils.generateKey<Intl.NumberFormatOptions>(this.locale, rules); const cacheKey = utils.generateKey<Intl.NumberFormatOptions>(this.locale, rules);

View File

@ -19,23 +19,25 @@ import { DatePool, Locale, Locales, SelectPool } from '../types/types';
import PluralFormatter from './fomatters/PluralFormatter'; import PluralFormatter from './fomatters/PluralFormatter';
import SelectFormatter from './fomatters/SelectFormatter'; import SelectFormatter from './fomatters/SelectFormatter';
import { FormatOptions, I18nCache, IntlMessageFormat } from '../types/interfaces'; import { FormatOptions, I18nCache, IntlMessageFormat } from '../types/interfaces';
import cache from './cache/cache';
/** /**
* *
*/ */
const generateFormatters = ( const generateFormatters = (
locale: Locale, locale: Locale | Locales,
locales: Locales, locales: Locales,
localeConfig: Record<string, any> = { plurals: undefined }, localeConfig: Record<string, any> = { plurals: undefined },
formatOptions: FormatOptions = {}, // 自定义格式对象 formatOptions: FormatOptions = {}, // 自定义格式对象
cache: I18nCache cache: I18nCache
): IntlMessageFormat => { ): IntlMessageFormat => {
locale = locales || locale;
const { plurals } = localeConfig; const { plurals } = localeConfig;
/** /**
* *
* @param formatOption * @param formatOption
*/ */
const getStyleOption = (formatOption: string | number) => { const getStyleOption = formatOption => {
if (typeof formatOption === 'string') { if (typeof formatOption === 'string') {
return formatOptions[formatOption] || { option: formatOption }; return formatOptions[formatOption] || { option: formatOption };
} else { } else {
@ -56,14 +58,14 @@ const generateFormatters = (
return pluralFormatter.replaceSymbol.bind(pluralFormatter); 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 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); return pluralFormatter.replaceSymbol.bind(pluralFormatter);
}, },
// 选择规则,如果规则对象中包含与该值相对应的属性,则返回该属性的值;否则,返回 "other" 属性的值。 // 选择规则,如果规则对象中包含与该值相对应的属性,则返回该属性的值;否则,返回 "other" 属性的值。
select: (value: SelectPool, formatRules: any) => { select: (value: SelectPool, formatRules) => {
const selectFormatter = new SelectFormatter(locale, cache); const selectFormatter = new SelectFormatter(locale, cache);
return selectFormatter.getRule(value, formatRules); return selectFormatter.getRule(value, formatRules);
}, },
@ -73,16 +75,17 @@ const generateFormatters = (
return new NumberFormatter(locales, getStyleOption(formatOption), cache).numberFormat(value); return new NumberFormatter(locales, getStyleOption(formatOption), cache).numberFormat(value);
}, },
// 用于将日期格式化为字符串,接受一个日期对象和一个格式化规则。它会根据规则返回格式化后的字符串。
/** /**
*
* eg: { year: 'numeric', month: 'long', day: 'numeric' } DateTimeFormatter如何将日期对象转换为字符串的参数 * eg: { year: 'numeric', month: 'long', day: 'numeric' } DateTimeFormatter如何将日期对象转换为字符串的参数
* \year: 'numeric' 2023 * \year: 'numeric' 2023
* month: 'long' January * month: 'long' January
* day: 'numeric' 1 * day: 'numeric' 1
* @param value * @param value
* @param formatOption { year: 'numeric', month: 'long', day: 'numeric' } * @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); return new DateTimeFormatter(locales, getStyleOption(formatOption), cache).dateTimeFormat(value, formatOption);
}, },

View File

@ -19,21 +19,19 @@ import I18n from '../core/I18n';
import { MessageDescriptor, MessageOptions } from '../types/interfaces'; import { MessageDescriptor, MessageOptions } from '../types/interfaces';
import { CompiledMessage } from '../types/types'; import { CompiledMessage } from '../types/types';
import creatI18nCache from './cache/cache'; import creatI18nCache from './cache/cache';
import { formatElements } from '../utils/formatElements';
export function getFormatMessage( export function getFormatMessage(
i18n: I18n, i18n: I18n,
id: MessageDescriptor | string, id: MessageDescriptor | string,
values: Record<string, unknown> | undefined = {}, values: Record<string, unknown> | undefined = {},
options: MessageOptions = {}, options: MessageOptions = {}
components: any
) { ) {
let { messages, context } = options; let { message, context } = options;
const { formatOptions } = options; const { formatOptions } = options;
const cache = i18n.cache ?? creatI18nCache(); const cache = i18n.cache ?? creatI18nCache();
if (typeof id !== 'string') { if (typeof id !== 'string') {
values = values || id.defaultValues; values = values || id.defaultValues;
messages = id.messages || id.defaultMessage; message = id.message || id.defaultMessage;
context = id.context; context = id.context;
id = id.id; id = id.id;
} }
@ -44,7 +42,7 @@ export function getFormatMessage(
const messageUnavailable = isMissingContextMessage || isMissingMessage; const messageUnavailable = isMissingContextMessage || isMissingMessage;
// 对错误消息进行处理 // 对错误消息进行处理
const messageError = i18n.onError; const messageError = i18n.error;
if (messageError && messageUnavailable) { if (messageError && messageUnavailable) {
if (typeof messageError === 'function') { if (typeof messageError === 'function') {
return messageError(i18n.locale, id, context); return messageError(i18n.locale, id, context);
@ -55,17 +53,14 @@ export function getFormatMessage(
let compliedMessage: CompiledMessage; let compliedMessage: CompiledMessage;
if (context) { if (context) {
compliedMessage = i18n.messages[context][id] || messages || id; compliedMessage = i18n.messages[context][id] || message || id;
} else { } 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; compliedMessage = typeof compliedMessage === 'string' ? utils.compile(compliedMessage) : compliedMessage;
const translation = new Translation(compliedMessage, i18n.locale, i18n.locales, i18n.localeConfig, cache); const translation = new Translation(compliedMessage, i18n.locale, i18n.locales, i18n.localeConfig, cache);
const formatResult = translation.translate(values, formatOptions); return translation.translate(values, formatOptions);
// 如果存在inula元素则返回包含格式化的Inula元素的数组
return formatElements(formatResult, components);
} }

View File

@ -16,12 +16,9 @@
import ruleUtils from '../utils/parseRuleUtils'; import ruleUtils from '../utils/parseRuleUtils';
import { LexerInterface } from '../types/interfaces'; import { LexerInterface } from '../types/interfaces';
/**
* message进行处理成Token
*/
class Lexer<T> implements LexerInterface<T> { class Lexer<T> implements LexerInterface<T> {
readonly startState: string; readonly startState: string;
readonly unionReg: Record<string, any>; readonly states: Record<string, any>;
private buffer = ''; private buffer = '';
private stack: string[] = []; private stack: string[] = [];
private index = 0; private index = 0;
@ -31,23 +28,19 @@ class Lexer<T> implements LexerInterface<T> {
private state = ''; private state = '';
private groups: string[] = []; private groups: string[] = [];
private error: Record<string, any> | undefined; private error: Record<string, any> | undefined;
private regexp: any; private regexp;
private fast: Record<string, unknown> = {}; private fast: Record<string, unknown> = {};
private queuedGroup: string | null = ''; private queuedGroup: string | null = '';
private value = ''; private value = '';
constructor(unionReg: Record<string, any>, startState: string) { constructor(unionReg: Record<string, any>, startState: string) {
this.startState = startState; this.startState = startState;
this.unionReg = unionReg; this.states = unionReg;
this.buffer = ''; this.buffer = '';
this.stack = []; this.stack = [];
this.reset(); this.reset();
} }
/**
*
* @param data
*/
public reset(data?: string) { public reset(data?: string) {
this.buffer = data || ''; this.buffer = data || '';
this.index = 0; this.index = 0;
@ -64,7 +57,7 @@ class Lexer<T> implements LexerInterface<T> {
return; return;
} }
this.state = state; this.state = state;
const info = this.unionReg[state]; const info = this.states[state];
this.groups = info.groups; this.groups = info.groups;
this.error = info.error; this.error = info.error;
this.regexp = info.regexp; this.regexp = info.regexp;
@ -80,7 +73,7 @@ class Lexer<T> implements LexerInterface<T> {
this.setState(state); this.setState(state);
} }
private getGroup(match: Record<string, object>) { private getGroup(match: Record<string, any>) {
const groupCount = this.groups.length; const groupCount = this.groups.length;
for (let i = 0; i < groupCount; i++) { for (let i = 0; i < groupCount; i++) {
if (match[i + 1] !== undefined) { if (match[i + 1] !== undefined) {
@ -94,9 +87,7 @@ class Lexer<T> implements LexerInterface<T> {
return this.value; return this.value;
} }
/** // 迭代获取下一个 token
* token
*/
public next() { public next() {
const index = this.index; const index = this.index;
@ -121,6 +112,7 @@ class Lexer<T> implements LexerInterface<T> {
const regexp = this.regexp; const regexp = this.regexp;
regexp.lastIndex = index; regexp.lastIndex = index;
const match = getMatch(regexp, buffer); const match = getMatch(regexp, buffer);
const error = this.error; const error = this.error;
if (match == null) { if (match == null) {
return this.getToken(error, buffer.slice(index, buffer.length), index); return this.getToken(error, buffer.slice(index, buffer.length), index);
@ -139,9 +131,9 @@ class Lexer<T> implements LexerInterface<T> {
} }
/** /**
* Token * Token
* @param group * @param group
* @param text * @param text
* @param offset * @param offset
* @private * @private
*/ */
@ -195,7 +187,7 @@ class Lexer<T> implements LexerInterface<T> {
return token; return token;
} }
// 增加迭代器,允许逐个访问集合中的元素方法 // 增加迭代器
[Symbol.iterator]() { [Symbol.iterator]() {
return { return {
next: (): IteratorResult<T> => { next: (): IteratorResult<T> => {
@ -206,15 +198,9 @@ class Lexer<T> implements LexerInterface<T> {
} }
} }
/**
* message的值
* 0
* 123
*/
const getMatch = ruleUtils.checkSticky() const getMatch = ruleUtils.checkSticky()
? // 正则表达式具有 sticky 标志 ? // 正则表达式具有 sticky 标志
(regexp: any, buffer: string) => regexp.exec(buffer) (regexp, buffer) => regexp.exec(buffer)
: // 正则表达式具有 global 标志,匹配的字符串长度为 0则表示匹配失败 : // 正则表达式具有 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; export default Lexer;

View File

@ -14,47 +14,40 @@
*/ */
const body: Record<string, any> = { const body: Record<string, any> = {
doubleapos: { match: "''", value: () => "'" }, doubleapos: { match: '\'\'', value: () => '\'' },
quoted: { quoted: {
lineBreaks: true, lineBreaks: true,
match: /'[{}#](?:[^]*?[^'])?'(?!')/u, // {}# 'Hello' {name}{}# match: /'[{}#](?:[^]*?[^'])?'(?!')/u,
value: (src: string) => src.slice(1, -1).replace(/''/g, "'"), value: src => src.slice(1, -1).replace(/''/g, '\''),
}, },
argument: { argument: {
lineBreaks: true, lineBreaks: true,
// 用于匹配{name、{Hello{World匹配{ }花括号中有任何Unicode字符如空格、制表符等
match: /\{\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*/u, match: /\{\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*/u,
push: 'arg', push: 'arg',
value: (src: string) => src.substring(1).trim(), value: src => src.substring(1).trim(),
}, },
octothorpe: '#', octothorpe: '#',
end: { match: '}', pop: 1 }, end: { match: '}', pop: 1 },
content: { content: { lineBreaks: true, match: /[^][^{}#']*/u },
lineBreaks: true,
match: /[^][^{}#]*/u, // []{}#
},
}; };
const arg: Record<string, any> = { const arg: Record<string, any> = {
select: { select: {
lineBreaks: true, lineBreaks: true,
match: /,\s*(?:plural|select|selectordinal)\s*,\s*/u, // pluralselect selectordinal match: /,\s*(?:plural|select|selectordinal)\s*,\s*/u,
next: 'select', // 继续解析下一个参数 next: 'select',
value: (src: string) => src.split(',')[1].trim(), // 提取第二个参数,并处理收尾空格 value: src => src.split(',')[1].trim(),
}, },
'func-args': { 'func-args': {
// 匹配是否包含其他非特殊字符的参数,匹配结果包含特殊字符如param1, param2, param3
lineBreaks: true, lineBreaks: true,
match: /,\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*,/u, match: /,\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*,/u,
next: 'body', next: 'body',
value: (src: string) => src.split(',')[1].trim(), // 参数字符串去除逗号并去除首尾空格 value: src => src.split(',')[1].trim(),
}, },
'func-simple': { 'func-simple': {
// 匹配是否包含其他简单参数匹配结果不包含标点符号param1 param2 param3
lineBreaks: true, lineBreaks: true,
match: /,\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*/u, 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 }, end: { match: '}', pop: 1 },
}; };
@ -62,17 +55,14 @@ const arg: Record<string, any> = {
const select: Record<string, any> = { const select: Record<string, any> = {
offset: { offset: {
lineBreaks: true, lineBreaks: true,
match: /\s*offset\s*:\s*\d+\s*/u, // messageoffest match: /\s*offset\s*:\s*\d+\s*/u,
value: (src: string) => src.split(':')[1].trim(), value: src => src.split(':')[1].trim(),
}, },
case: { case: {
// 检查匹配该行是否包含分支信息。
lineBreaks: true, lineBreaks: true,
// 设置规则匹配以左大括号 { 结尾的字符串,以等号 = 后跟数字开头的字符串,或者以非特殊符号和非空白字符开头的字符串,如 '=1 {'
match: /\s*(?:=\d+|[^\p{Pat_Syn}\p{Pat_WS}]+)\s*\{/u, match: /\s*(?:=\d+|[^\p{Pat_Syn}\p{Pat_WS}]+)\s*\{/u,
push: 'body', // 匹配成功则会push到body栈中 push: 'body',
value: (src: string) => src.substring(0, src.indexOf('{')).trim(), value: src => src.substring(0, src.indexOf('{')).trim(),
}, },
end: { match: /\s*\}/u, pop: 1 }, end: { match: /\s*\}/u, pop: 1 },
}; };

View File

@ -17,13 +17,12 @@ import Lexer from './Lexer';
import { mappingRule } from './mappingRule'; import { mappingRule } from './mappingRule';
import ruleUtils from '../utils/parseRuleUtils'; import ruleUtils from '../utils/parseRuleUtils';
import { RawToken } from '../types/types'; 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 }); const defaultErrorRule = ruleUtils.getRuleOptions('error', { lineBreaks: true, shouldThrow: true });
// 解析规则并生成词法分析器所需的数据结构,以便进行词法分析操作 // 解析规则并生成词法分析器所需的数据结构,以便进行词法分析操作
function parseRules(rules: Record<string, any>, hasStates: boolean): Record<string, object> { function parseRules(rules: Record<string, any>, hasStates: boolean): Record<string, any> {
let errorRule: Record<string, object> | null = null; let errorRule: Record<string, any> | null = null;
const fast: Record<string, unknown> = {}; const fast: Record<string, unknown> = {};
let enableFast = true; let enableFast = true;
let unicodeFlag: boolean | null = null; let unicodeFlag: boolean | null = null;
@ -59,7 +58,7 @@ function parseRules(rules: Record<string, any>, hasStates: boolean): Record<stri
groups.push(options); groups.push(options);
// 检查是否所有规则都使用了unicode或者都未使用 // 检查是否所有规则都使用了 unicode 标志,或者都未使用
unicodeFlag = checkUnicode(match, unicodeFlag, options); unicodeFlag = checkUnicode(match, unicodeFlag, options);
const pat = ruleUtils.getRegUnion(match.map(ruleUtils.getReg)); 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 标志,则使用无法被否定的空模式来模拟 // 如果没有 fallback 规则,则使用 sticky 标志,只在当前索引位置寻找匹配项,如果不支持 sticky 标志,则使用无法被否定的空模式来模拟
const fallbackRule = errorRule && errorRule.fallback; const fallbackRule = errorRule && errorRule.fallback;
let flags = ruleUtils.checkSticky() && !fallbackRule ? STICKY_FLAG : GLOBAL_FLAG; let flags = ruleUtils.checkSticky() && !fallbackRule ? 'ym' : 'gm';
const suffix = ruleUtils.checkSticky() || fallbackRule ? '' : VERTICAL_LINE; const suffix = ruleUtils.checkSticky() || fallbackRule ? '' : '|';
if (unicodeFlag === true) { if (unicodeFlag === true) {
flags += UNICODE_FLAG; flags += 'u';
} }
const combined = new RegExp(ruleUtils.getRegUnion(parts) + suffix, flags); 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); const state = group && (group.push || group.next);
if (state && !mappingRules[state]) { if (state && !map[state]) {
throw new Error('The state is missing.'); 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.'); 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); const keys = Object.getOwnPropertyNames(mappingRule);
if (!startState) { if (!startState) {
@ -134,7 +133,7 @@ function parseMappingRule(mappingRule: Record<string, object>, startState?: stri
continue; continue;
} }
const splice = [j, STATE_GROUP_START_INDEX]; const splice = [j, 1];
if (rule.include !== key && !included[rule.include]) { if (rule.include !== key && !included[rule.include]) {
included[rule.include] = true; included[rule.include] = true;
const newRules = ruleMap[rule.include]; const newRules = ruleMap[rule.include];
@ -175,30 +174,17 @@ function parseMappingRule(mappingRule: Record<string, object>, startState?: stri
}); });
}); });
// 将规则注入到词法解析器
return new Lexer(mappingAllRules, startState); return new Lexer(mappingAllRules, startState);
} }
/** function processFast(match, fast: Record<string, unknown>, options) {
*
* @param match
* @param fast
* @param options
*/
function processFast(match: Record<string, any>, fast: Record<string, unknown> = {}, options: Record<string, object>) {
while (match.length && typeof match[0] === 'string' && match[0].length === 1) { while (match.length && typeof match[0] === 'string' && match[0].length === 1) {
// 获取到数组的第一个元素
const word = match.shift(); const word = match.shift();
fast[word.charCodeAt(0)] = options; fast[word.charCodeAt(0)] = options;
} }
} }
/** function handleErrorRule(options, errorRule: Record<string, any>) {
*
* @param options
* @param errorRule
*/
function handleErrorRule(options: Record<string, object>, errorRule: Record<string, object>) {
if (!options.fallback === !errorRule.fallback) { if (!options.fallback === !errorRule.fallback) {
throw new Error('errorRule can only set one!'); throw new Error('errorRule can only set one!');
} else { } else {
@ -206,13 +192,7 @@ function handleErrorRule(options: Record<string, object>, errorRule: Record<stri
} }
} }
/** function checkUnicode(match, unicodeFlag, options) {
* message中是否包含Unicode
* @param match message
* @param unicodeFlag Unicode标志
* @param options
*/
function checkUnicode(match: Record<string, any>, unicodeFlag: boolean | null, options: Record<string, any>) {
for (let j = 0; j < match.length; j++) { for (let j = 0; j < match.length; j++) {
const obj = match[j]; const obj = match[j];
if (!ruleUtils.checkRegExp(obj)) { if (!ruleUtils.checkRegExp(obj)) {
@ -221,16 +201,14 @@ function checkUnicode(match: Record<string, any>, unicodeFlag: boolean | null, o
if (unicodeFlag === null) { if (unicodeFlag === null) {
unicodeFlag = obj.unicode; unicodeFlag = obj.unicode;
} else { } else if (unicodeFlag !== obj.unicode && options.fallback === false) {
if (unicodeFlag !== obj.unicode && options.fallback === false) { throw new Error('If the /u flag is used, all!');
throw new Error('If the /u flag is used, all!');
}
} }
} }
return unicodeFlag; return unicodeFlag;
} }
function checkStateOptions(hasStates: boolean, options: Record<string, any>) { function checkStateOptions(hasStates: boolean, options) {
if (!hasStates) { if (!hasStates) {
throw new Error('State toggle options are not allowed in stateless tokenizers!'); 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) { function isExistsFallback(rules: Record<string, any>, enableFast: boolean) {
for (let i = 0; i < rules.length; i++) { for (let i = 0; i < rules.length; i++) {
if (rules[i].fallback) { if (rules[i].fallback) {
@ -253,7 +226,7 @@ function isExistsFallback(rules: Record<string, any>, enableFast: boolean) {
return enableFast; 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) { if (options.error || options.fallback) {
// 只能设置一个 errorRule // 只能设置一个 errorRule
if (errorRule) { if (errorRule) {

View File

@ -14,13 +14,23 @@
*/ */
import { lexer } from './parseMappingRule'; import { lexer } from './parseMappingRule';
import { RawToken } from '../types/types'; import { RawToken, Token } from '../types/types';
import { DEFAULT_PLURAL_KEYS } from '../constants'; import { DEFAULT_PLURAL_KEYS } from '../constants';
import { Content, FunctionArg, PlainArg, Select, TokenContext } from '../types/interfaces'; 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 { class Parser {
cardinalKeys: string[] = DEFAULT_PLURAL_KEYS; cardinalKeys: string[] = DEFAULT_PLURAL_KEYS;
ordinalKeys: string[] = DEFAULT_PLURAL_KEYS; ordinalKeys: string[] = DEFAULT_PLURAL_KEYS;
@ -29,7 +39,7 @@ class Parser {
lexer.reset(message); lexer.reset(message);
} }
isSelectKeyValid(type: Select['type'], value: string) { isSelectKeyValid(token: RawToken, type: Select['type'], value: string) {
if (value[0] === '=') { if (value[0] === '=') {
if (type === 'select') { if (type === 'select') {
throw new Error('The key value of the select type is invalid.'); throw new Error('The key value of the select type is invalid.');
@ -65,7 +75,7 @@ class Parser {
break; break;
} }
case 'case': { case 'case': {
this.isSelectKeyValid(type, token.value); this.isSelectKeyValid(token, type, token.value);
select.cases.push({ select.cases.push({
key: token.value.replace(/=/g, ''), key: token.value.replace(/=/g, ''),
tokens: this.parse(isPlural), tokens: this.parse(isPlural),
@ -84,11 +94,6 @@ class Parser {
throw new Error('The message end position is invalid.'); throw new Error('The message end position is invalid.');
} }
/**
* Token
* @param token
* @param isPlural
*/
parseToken(token: RawToken, isPlural: boolean): PlainArg | FunctionArg | Select { parseToken(token: RawToken, isPlural: boolean): PlainArg | FunctionArg | Select {
const context = getContext(token); const context = getContext(token);
const nextToken = lexer.next(); const nextToken = lexer.next();
@ -148,12 +153,7 @@ class Parser {
} }
} }
/** // 在根级别解析时,遇到结束符号即结束解析并返回结果;而在非根级别解析时,遇到结束符号会被视为不合法的结束位置,抛出错误
*
*
* @param isPlural
* @param isRoot
*/
parse(isPlural: boolean, isRoot?: boolean): Array<Content | PlainArg | FunctionArg | Select> { parse(isPlural: boolean, isRoot?: boolean): Array<Content | PlainArg | FunctionArg | Select> {
const tokens: any[] = []; const tokens: any[] = [];
let content: string | Content | null = null; 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> { export default function parse(message: string): Array<Content | PlainArg | FunctionArg | Select> {
const parser = new Parser(message); const parser = new Parser(message);
return parser.parse(false, true); return parser.parse(false, true);

View File

@ -13,25 +13,15 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import { import { AllLocaleConfig, AllMessages, Locale, Locales, Error, DatePool, SelectPool, RawToken } from './types';
AllLocaleConfig,
AllMessages,
Locale,
Locales,
Error,
DatePool,
SelectPool,
RawToken,
InulaNode,
} from './types';
import I18n from '../core/I18n'; import I18n from '../core/I18n';
import Lexer from '../parser/Lexer'; import Lexer from '../parser/Lexer';
import { InulaElement, Key } from 'openinula';
// FormattedMessage的参数定义 // FormattedMessage的参数定义
export interface FormattedMessageProps extends MessageDescriptor { export interface FormattedMessageProps extends MessageDescriptor {
values?: Record<string, unknown>; values?: Record<string, unknown>;
tagName?: string; tagName?: string;
children?(nodes: any[]): any; children?(nodes: any[]): any;
} }
@ -44,7 +34,7 @@ export interface MessageDescriptor extends MessageOptions {
export interface MessageOptions { export interface MessageOptions {
comment?: string; comment?: string;
messages?: string; message?: string;
context?: string; context?: string;
formatOptions?: FormatOptions; formatOptions?: FormatOptions;
} }
@ -58,26 +48,15 @@ export interface I18nCache {
octothorpe: Record<string, any>; octothorpe: Record<string, any>;
} }
export interface RichText {
components?: { [key: string]: InulaNode };
}
export interface InulaPortal extends InulaElement {
key: Key | null;
children: InulaNode;
}
// I18n类的传参 // I18n类的传参
export type I18nProps = RichText & { export interface I18nProps {
locale?: Locale; locale?: Locale;
locales?: Locales; locales?: Locales;
messages?: AllMessages; messages?: AllMessages;
defaultLocale?: string;
timeZone?: string;
localeConfig?: AllLocaleConfig; localeConfig?: AllLocaleConfig;
cache?: I18nCache; cache?: I18nCache;
onError?: Error; error?: Error;
}; }
// 消息格式化选项类型 // 消息格式化选项类型
export interface FormatOptions { export interface FormatOptions {
@ -95,13 +74,16 @@ export interface I18nContextProps {
i18n?: I18n; i18n?: I18n;
} }
export type configProps = I18nProps & { export interface configProps {
locale?: Locale;
messages?: AllMessages;
defaultLocale?: string;
RenderOnLocaleChange?: boolean; RenderOnLocaleChange?: boolean;
children?: any; children?: any;
onWarn?: Error; onWarn?: Error;
}; }
export interface IntlMessageFormat { export interface IntlMessageFormat extends configProps, MessageOptions {
plural: ( plural: (
value: number, value: number,
{ {
@ -222,6 +204,7 @@ export interface InjectedIntl {
formatMessage( formatMessage(
messageDescriptor: MessageDescriptor, messageDescriptor: MessageDescriptor,
values?: Record<string, unknown>, values?: Record<string, unknown>,
options?: MessageOptions options?: MessageOptions,
): string | any[]; useMemorize?: boolean
): string;
} }

View File

@ -23,17 +23,16 @@ import {
I18nContextProps, I18nContextProps,
configProps, configProps,
InjectedIntl, InjectedIntl,
InulaPortal,
} from './interfaces'; } 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 Locale = string;
export type Locales = Locale | Locale[]; 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>; 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 DatePool = Date | string;
export type SelectPool = string | number; export type SelectPool = string | Record<string, unknown>;
export type RawToken = { export type RawToken = {
type: string; type: string;
@ -75,23 +74,13 @@ export type RawToken = {
export type I18nProviderProps = I18nContextProps & configProps; export type I18nProviderProps = I18nContextProps & configProps;
export type IntlType = I18nContextProps & { export type IntlType = {
defaultLocale?: string | undefined; i18n: I18n;
onError?: Error | undefined;
messages?:
| string
| Record<string, string>
| Record<string, string | CompiledMessagePart[]>
| Record<string, Record<string, string> | Record<string, string | CompiledMessagePart[]>>;
locale?: string;
formatMessage: (...args: any[]) => any; formatMessage: (...args: any[]) => any;
formatNumber: (...args: any[]) => any; formatNumber: (...args: any[]) => any;
formatDate: (...args: any[]) => any; formatDate: (...args: any[]) => any;
$t?: (...args: any[]) => any;
}; };
export type InjectedIntlProps = { export interface InjectedIntlProps {
intl: InjectedIntl; intl: InjectedIntl;
}; }
export type InulaNode = InulaElement | string | number | Iterable<InulaNode> | InulaPortal | boolean | null | undefined;

View File

@ -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');
// valueThis 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++}`;
};
}

View File

@ -18,7 +18,6 @@ function getType(input: any): string {
return str.slice(8, -1).toLowerCase(); return str.slice(8, -1).toLowerCase();
} }
// 类型检查器
const createTypeChecker = (type: string) => { const createTypeChecker = (type: string) => {
return (input: any) => { return (input: any) => {
return getType(input) === type.toLowerCase(); return getType(input) === type.toLowerCase();
@ -29,25 +28,24 @@ const checkObject = (input: any) => input !== null && typeof input === 'object';
const checkRegExp = createTypeChecker('RegExp'); const checkRegExp = createTypeChecker('RegExp');
// 使用正则表达式如果对象存在则访问该属性用来判断当前环境是否支持正则表达式sticky属性。
const checkSticky = () => typeof new RegExp('')?.sticky === 'boolean'; const checkSticky = () => typeof new RegExp('')?.sticky === 'boolean';
// 转义正则表达式中的特殊字符 // 转义正则表达式中的特殊字符
function transferReg(str: string): string { function transferReg(s: string): string {
// eslint-disable-next-line // eslint-disable-next-line
return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
} }
// 计算正则表达式中捕获组的数量,用以匹配() // 计算正则表达式中捕获组的数量
function getRegGroups(str: string): number { function getRegGroups(s: string): number {
const regExp = new RegExp('|' + str); const re = new RegExp('|' + s);
// eslint-disable-next-line // eslint-disable-next-line
return regExp.exec('')?.length! - 1; return re.exec('')?.length! - 1;
} }
// 创建一个捕获组的正则表达式模式 // 创建一个捕获组的正则表达式模式
function getRegCapture(str: string): string { function getRegCapture(s: string): string {
return '(' + str + ')'; return '(' + s + ')';
} }
// 将正则表达式合并为一个联合的正则表达式模式 // 将正则表达式合并为一个联合的正则表达式模式
@ -55,7 +53,7 @@ function getRegUnion(regexps: string[]): string {
if (!regexps.length) { if (!regexps.length) {
return '(?!)'; return '(?!)';
} }
const source = regexps.map(str => '(?:' + str + ')').join('|'); const source = regexps.map(s => '(?:' + s + ')').join('|');
return '(?:' + source + ')'; return '(?:' + source + ')';
} }
@ -145,7 +143,7 @@ function getRulesByArray(array: any[]) {
return result; return result;
} }
function getRuleOptions(type: any, obj: any) { function getRuleOptions(type, obj) {
// 如果 obj 不是一个对象,则将其转换为包含 'match' 属性的对象 // 如果 obj 不是一个对象,则将其转换为包含 'match' 属性的对象
if (!checkObject(obj)) { if (!checkObject(obj)) {
obj = { match: obj }; obj = { match: obj };
@ -184,23 +182,23 @@ function getRuleOptions(type: any, obj: any) {
} else { } else {
options.match = []; 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; return 0;
} else if (checkRegExp(str2)) { } else if (checkRegExp(b)) {
return -1; return -1;
} else if (checkRegExp(str1)) { } else if (checkRegExp(a)) {
return +1; return +1;
} else { } else {
return str2.length - str1.length; return b.length - a.length;
} }
}); });
return options; return options;
} }
function getRules(spec: any) { function getRules(spec) {
return Array.isArray(spec) ? getRulesByArray(spec) : getRulesByObject(spec); return Array.isArray(spec) ? getRulesByArray(spec) : getRulesByObject(spec);
} }

View File

@ -32,7 +32,7 @@ function compile(message: string): CompiledMessage {
try { try {
return getTokenAST(parse(message)); return getTokenAST(parse(message));
} catch (e) { } 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; return message;
} }
} }

View File

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

View File

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

View File

@ -43,7 +43,7 @@ describe('<FormattedMessage>', () => {
); );
setTimeout(() => { setTimeout(() => {
expect(getByTestId('id').textContent).toEqual(i18n.formatMessage('hello', {}, {})); expect(getByTestId('id')).toHaveTextContent(i18n.formatMessage('hello', '', {}));
}, 1000); }, 1000);
}); });
it('should format context', function () { it('should format context', function () {
@ -58,6 +58,6 @@ describe('<FormattedMessage>', () => {
</span> </span>
</I18nProvider> </I18nProvider>
); );
expect(getByTestId('id').textContent).toEqual(i18n.formatMessage('id', { name: 'fred' }, {})); expect(getByTestId('id')).toHaveTextContent(i18n.formatMessage('id', { name: 'fred' }, {}));
}); });
}); });

View File

@ -42,6 +42,7 @@ describe('InjectIntl', () => {
jest.spyOn(console, 'error').mockImplementation(() => {}); jest.spyOn(console, 'error').mockImplementation(() => {});
const Injected = injectIntl(Wrapped); const Injected = injectIntl(Wrapped);
// @ts-ignore
expect(() => render(<Injected />)).toThrow("Cannot read properties of null (reading 'i18n')"); expect(() => render(<Injected />)).toThrow("Cannot read properties of null (reading 'i18n')");
}); });
@ -52,7 +53,7 @@ describe('InjectIntl', () => {
}; };
const { getByTestId } = mountWithProvider(<Injected {...props} />); const { getByTestId } = mountWithProvider(<Injected {...props} />);
expect(JSON.stringify(getByTestId('test'))).toEqual( expect(getByTestId('test')).toHaveTextContent(
'{"_events":{},"locale":"en","locales":["en"],"allMessages":{},"_localeData":{}}' '{"_events":{},"locale":"en","locales":["en"],"allMessages":{},"_localeData":{}}'
); );
}); });

View File

@ -29,20 +29,6 @@ describe('createI18n', () => {
).toBe('bar'); ).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 () { it('should not warn when defaultRichTextElements is not used', function () {
const onWarn = jest.fn(); const onWarn = jest.fn();
createI18n({ createI18n({

View File

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

View File

@ -15,7 +15,7 @@
import creatI18nCache from '../../../src/format/cache/cache'; import creatI18nCache from '../../../src/format/cache/cache';
describe('creatI18nCache', () => { describe('creatI18nCache', () => {
it('should create an empty I18nCache object', () => { it('should create an empty IntlCache object', () => {
const intlCache = creatI18nCache(); const intlCache = creatI18nCache();
expect(intlCache).toEqual({ expect(intlCache).toEqual({

View File

@ -61,7 +61,7 @@ describe('DateTimeFormatter', () => {
expect(spy).toHaveBeenCalledWith('en-GB', { month: 'short' }); 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 spy = jest.spyOn(Intl, 'DateTimeFormat');
const formatter1 = new DateTimeFormatter('en-US', { month: 'short' }); const formatter1 = new DateTimeFormatter('en-US', { month: 'short' });
const formatter2 = 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'); 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 formatter = new DateTimeFormatter('en-US', { year: 'numeric' }, creatI18nCache());
const date = new Date(2023, 0, 1); const date = new Date(2023, 0, 1);
const formatted1 = formatter.dateTimeFormat(date); const formatted1 = formatter.dateTimeFormat(date);

View File

@ -24,7 +24,7 @@ describe('getFormatMessage', () => {
}, },
}, },
locale: 'en', locale: 'en',
onError: 'missingMessage', error: 'missingMessage',
}); });
it('should return the correct translation for an existing message ID', () => { it('should return the correct translation for an existing message ID', () => {
@ -32,7 +32,7 @@ describe('getFormatMessage', () => {
const values = { name: 'John' }; const values = { name: 'John' };
const expectedResult = 'Hello, John!'; const expectedResult = 'Hello, John!';
const result = getFormatMessage(i18nInstance, id, values, {}, {}); const result = getFormatMessage(i18nInstance, id, values);
expect(result).toEqual(expectedResult); expect(result).toEqual(expectedResult);
}); });
@ -41,7 +41,7 @@ describe('getFormatMessage', () => {
const id = 'missingMessage'; const id = 'missingMessage';
const expectedResult = 'missingMessage'; const expectedResult = 'missingMessage';
const result = getFormatMessage(i18nInstance, id, {}, {}, {}); const result = getFormatMessage(i18nInstance, id);
expect(result).toEqual(expectedResult); expect(result).toEqual(expectedResult);
}); });

View File

@ -15,7 +15,7 @@
import copyStaticProps from '../../src/utils/copyStaticProps'; import copyStaticProps from '../../src/utils/copyStaticProps';
describe('copyStaticProps', () => { describe('copyStaticProps', () => {
it('should hoist static properties from sourceComponent to targetComponent', () => { test('should hoist static properties from sourceComponent to targetComponent', () => {
class SourceComponent { class SourceComponent {
static staticProp = 'sourceProp'; static staticProp = 'sourceProp';
} }
@ -23,10 +23,11 @@ describe('copyStaticProps', () => {
class TargetComponent {} class TargetComponent {}
copyStaticProps(TargetComponent, SourceComponent); copyStaticProps(TargetComponent, SourceComponent);
expect((TargetComponent as any).staticProp).toBe('sourceProp'); 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 { class SourceComponent {
static staticProp = 'sourceProp'; static staticProp = 'sourceProp';
} }
@ -36,10 +37,11 @@ describe('copyStaticProps', () => {
class TargetComponent {} class TargetComponent {}
copyStaticProps(TargetComponent, InheritedComponent); copyStaticProps(TargetComponent, InheritedComponent);
expect((TargetComponent as any).staticProp).toBe('sourceProp'); 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 { class SourceComponent {
get staticProp() { get staticProp() {
return 'sourceProp'; return 'sourceProp';
@ -49,10 +51,11 @@ describe('copyStaticProps', () => {
class TargetComponent {} class TargetComponent {}
copyStaticProps(TargetComponent, SourceComponent); copyStaticProps(TargetComponent, SourceComponent);
expect((TargetComponent as any).staticProp).toBeUndefined(); 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 { class SourceComponent {
static get staticProp() { static get staticProp() {
return 'sourceProp'; return 'sourceProp';
@ -62,10 +65,11 @@ describe('copyStaticProps', () => {
class TargetComponent {} class TargetComponent {}
copyStaticProps(TargetComponent, SourceComponent); copyStaticProps(TargetComponent, SourceComponent);
expect((TargetComponent as any).staticProp).toBe('sourceProp'); 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 targetComponent = { staticProp: 'target' };
const sourceComponent = { staticProp: 'source' }; const sourceComponent = { staticProp: 'source' };
copyStaticProps(targetComponent, sourceComponent); copyStaticProps(targetComponent, sourceComponent);

View File

@ -43,7 +43,7 @@ describe('eventEmitter', () => {
expect(listener).not.toBeCalled(); 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 unknown = jest.fn();
const emitter = new EventEmitter(); const emitter = new EventEmitter();

View File

@ -31,7 +31,6 @@
"declaration": true, "declaration": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"downlevelIteration": true, "downlevelIteration": true,
"emitDeclarationOnly": true,
"declarationDir": "./build/@types", "declarationDir": "./build/@types",
// 使@types/node // 使@types/node
"lib": [ "lib": [
@ -55,8 +54,7 @@
} }
}, },
"include": [ "include": [
"./index.ts", "./index.ts"
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules",

View File

@ -12,18 +12,16 @@
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import path from 'path';
import HtmlWebpackPlugin from 'html-webpack-plugin'; const { resolve } = require('path');
import { fileURLToPath } from 'url'; const HtmlWebpackPlugin = require('html-webpack-plugin');
const isDevelopment = process.env.NODE_ENV === 'development'; const isDevelopment = process.env.NODE_ENV === 'development';
const entryPath = './example/index.tsx'; const entryPath = './example/index.tsx';
const __filename = fileURLToPath(import.meta.url); module.exports = {
const __dirname = path.dirname(__filename); entry: resolve(__dirname, entryPath),
export default {
entry: path.join(__dirname, entryPath),
output: { output: {
path: path.join(__dirname, './build'), path: resolve(__dirname, './build'),
filename: 'main.js', filename: 'main.js',
}, },
module: { module: {
@ -52,7 +50,7 @@ export default {
mode: isDevelopment ? 'development' : 'production', mode: isDevelopment ? 'development' : 'production',
plugins: [ plugins: [
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: path.join(__dirname, './example/index.html'), template: resolve(__dirname, './example/index.html'),
}), }),
], ],
resolve: { resolve: {

View File

@ -14,5 +14,11 @@
*/ */
module.exports = { module.exports = {
presets: [['@babel/preset-env', { targets: { node: 'current' } }], ['@babel/preset-typescript']], presets: [
[
'@babel/preset-env',
{ targets: { node: 'current' }},
],
['@babel/preset-typescript'],
],
}; };

View File

@ -16,24 +16,26 @@
</div> </div>
<script src="../../dist/bundle.js"></script> <script src="../../dist/bundle.js"></script>
<script> <script>
import inulaRequest from "../../index";
const sendRequestButton = document.getElementById('sendRequestButton'); const sendRequestButton = document.getElementById('sendRequestButton');
const cancelRequestButton = document.getElementById('cancelRequestButton'); const cancelRequestButton = document.getElementById('cancelRequestButton');
const message = document.getElementById('message'); const message = document.getElementById('message');
let controller = new AbortController(); let controller = new AbortController;
const signal = controller.signal; const signal = controller.signal;
sendRequestButton.addEventListener('click', function() { sendRequestButton.addEventListener('click', function() {
setInterval(() => { setInterval(function() {
inulaRequest.get('http://localhost:3001/data', { inulaRequest.get('http://localhost:3001/data', {
signal signal
}).then(function(response) { }).then(function(response) {
message.innerHTML = '请求成功: ' + JSON.stringify(response.data, null, 2); message.innerHTML = '请求成功: ' + JSON.stringify(response.data, null, 2);
}).catch(function(error) { }).catch(function(error) {
if (inulaRequest.isCancel(error)) { if (inulaRequest.isCancel(error)) {
message.innerHTML = '请求已被取消:' + error.message; message.innerHTML = '请求已被取消: ' + error.message;
} else { } else {
message.innerHTML = '请求出错:' + error.message; message.innerHTML = '请求出错: ' + error.message;
} }
}); });
}, 1000) }, 1000)

View File

@ -118,7 +118,7 @@
putResult.innerHTML = JSON.stringify(error, null, 2); 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) { .then(function (response) {
deleteResult.innerHTML = JSON.stringify(response.data, null, 2); deleteResult.innerHTML = JSON.stringify(response.data, null, 2);
}) })

View File

@ -13,11 +13,11 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import express from 'express'; import express from "express";
import * as fs from 'fs'; import * as fs from "fs";
import * as path from 'path'; import bodyParser from "body-parser";
import bodyParser from 'body-parser'; import cors from "cors";
import cors from 'cors'; import * as path from "path";
const app = express(); const app = express();
const port = 3001; const port = 3001;

View File

@ -14,6 +14,7 @@
*/ */
import inulaRequest from './src/inulaRequest'; import inulaRequest from './src/inulaRequest';
import useIR from './src/core/useIR/useIR';
const { const {
create, create,
@ -59,6 +60,7 @@ export {
isIrError, isIrError,
spread, spread,
IrHeaders, IrHeaders,
useIR,
// 兼容axios // 兼容axios
Axios, Axios,
AxiosError, AxiosError,

View File

@ -11,7 +11,7 @@
"server": "nodemon .\\examples\\server\\serverTest.mjs" "server": "nodemon .\\examples\\server\\serverTest.mjs"
}, },
"files": [ "files": [
"dist", "/dist",
"README.md", "README.md",
"CHANGELOG.md" "CHANGELOG.md"
], ],
@ -26,25 +26,38 @@
"author": "", "author": "",
"license": "MulanPSL2", "license": "MulanPSL2",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.21.8",
"@babel/preset-env": "^7.21.5",
"@babel/preset-react": "^7.9.4", "@babel/preset-react": "^7.9.4",
"@babel/preset-typescript": "^7.21.4",
"@rollup/plugin-commonjs": "^19.0.0", "@rollup/plugin-commonjs": "^19.0.0",
"@types/jest": "^29.2.5",
"@types/react": "^17.0.34", "@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", "babel-loader": "^9.1.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"core-js": "3.32.1", "core-js": "3.32.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"eslint": "^8.31.0",
"express": "^4.18.2", "express": "^4.18.2",
"html-webpack-plugin": "^5.5.3", "html-webpack-plugin": "^5.5.3",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.4.1", "jest-environment-jsdom": "^29.4.1",
"jsdom": "^22.0.0", "jsdom": "^22.0.0",
"nodemon": "^2.0.22", "nodemon": "^2.0.22",
"prettier": "^2.6.2",
"rollup": "^3.20.2", "rollup": "^3.20.2",
"rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.34.1", "rollup-plugin-typescript2": "^0.34.1",
"ts-jest": "^29.0.4",
"ts-loader": "^9.4.2", "ts-loader": "^9.4.2",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tslib": "^2.5.0", "tslib": "^2.5.0",
"typescript": "^4.9.4",
"webpack": "^5.81.0", "webpack": "^5.81.0",
"webpack-cli": "^5.0.2", "webpack-cli": "^5.0.2",
"webpack-dev-server": "^4.13.3" "webpack-dev-server": "^4.13.3"

View File

@ -21,19 +21,16 @@ import { babel } from '@rollup/plugin-babel';
export default { export default {
input: './index.ts', input: './index.ts',
output: [ output: [{
{ file: 'dist/inulaRequest.js',
file: 'dist/inulaRequest.js', format: 'umd',
format: 'umd', exports: 'named',
exports: 'named', name: 'inulaRequest',
name: 'inulaRequest', sourcemap: false,
sourcemap: false, }, {
}, file: 'dist/inulaRequest.esm-browser.js',
{ format: 'esm',
file: 'dist/inulaRequest.esm-browser.js', }],
format: 'esm',
},
],
plugins: [ plugins: [
resolve(), resolve(),
commonjs(), commonjs(),

Some files were not shown because too many files have changed in this diff Show More