diff --git a/package.json b/package.json index 087ef97f9..7972054ce 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "build:inula-intl": "pnpm -F inula-intl build", "build:inula-request": "pnpm -F inula-request build", "build:inula-router": "pnpm -F inula-router build", + "build:inula-adapter": "pnpm -F inula-adapter build", "commitlint": "commitlint --config commitlint.config.js -e", "version": "pnpm exec changeset version && node packages/inula/scripts/sync-version.js", "postinstall": "husky install" diff --git a/packages/inula-adapter/.gitignore b/packages/inula-adapter/.gitignore new file mode 100644 index 000000000..3e75dddd8 --- /dev/null +++ b/packages/inula-adapter/.gitignore @@ -0,0 +1,5 @@ +/node_modules +.idea +.vscode +package-lock.json +/build diff --git a/packages/inula-adapter/.prettierrc.js b/packages/inula-adapter/.prettierrc.js new file mode 100644 index 000000000..c2622eaa5 --- /dev/null +++ b/packages/inula-adapter/.prettierrc.js @@ -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文件中的 \ No newline at end of file diff --git a/packages/inula-adapter/examples/$children/FruitList.jsx b/packages/inula-adapter/examples/$children/FruitList.jsx new file mode 100644 index 000000000..423a36631 --- /dev/null +++ b/packages/inula-adapter/examples/$children/FruitList.jsx @@ -0,0 +1,28 @@ +import FruitItem from './FruitItem.jsx'; + +export default function (props) { + const fruits = ['apple', 'pear', 'motorcycle']; + + function logFruit() { + console.log( + useInstance() + .$children.map(({ fruitName, isChecked }) => `${fruitName}:${isChecked ? '✓' : 'X'}`) + .join(', ') //returns string like "apple:✓, pear:X, motorcycle:✓" + ); + } + + return ( +
+ + +
+ ); +} diff --git a/packages/inula-adapter/examples/$children/FruitList.vue b/packages/inula-adapter/examples/$children/FruitList.vue new file mode 100644 index 000000000..4dfbc958e --- /dev/null +++ b/packages/inula-adapter/examples/$children/FruitList.vue @@ -0,0 +1,18 @@ + + + \ No newline at end of file diff --git a/packages/inula-adapter/examples/$el/TypewriterInput.jsx b/packages/inula-adapter/examples/$el/TypewriterInput.jsx new file mode 100644 index 000000000..e3ca65d9d --- /dev/null +++ b/packages/inula-adapter/examples/$el/TypewriterInput.jsx @@ -0,0 +1,18 @@ +import { playSound } from 'soundPlayer1'; +import { useInstance } from 'vue-inula'; + +export default function (props) { + const instance = useInstance(); + //use of $el must be delayed, because element representation is not available before render + setTimeout(() => { + instance.$el.addListener('keyDown', () => { + playSound('typewritterPush'); + }); + + instance.$el.addListener('keyUp', () => { + playSound('typewritterRelease'); + }); + }, 100); + + return ; +} diff --git a/packages/inula-adapter/examples/$el/TypewriterInput.vue b/packages/inula-adapter/examples/$el/TypewriterInput.vue new file mode 100644 index 000000000..e376968af --- /dev/null +++ b/packages/inula-adapter/examples/$el/TypewriterInput.vue @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/packages/inula-adapter/examples/$forceUpdate/clock.jsx b/packages/inula-adapter/examples/$forceUpdate/clock.jsx new file mode 100644 index 000000000..5496849a8 --- /dev/null +++ b/packages/inula-adapter/examples/$forceUpdate/clock.jsx @@ -0,0 +1,15 @@ +export default function (props) { + // useeffect and clearInterval operation mas no sense, but this is necessary + // to prevent problems between persistent vue components and temporary inula function components + useEffect(() => { + const [b, r] = useState(false); // by toggling this state object, force update is triggered + const interval = setInterval(() => { + r(!b); + }, 1000); + + return () => { + clearInterval(interval); + }; + }); + return {Date.now()}; +} diff --git a/packages/inula-adapter/examples/$forceUpdate/clock.vue b/packages/inula-adapter/examples/$forceUpdate/clock.vue new file mode 100644 index 000000000..97a7c870d --- /dev/null +++ b/packages/inula-adapter/examples/$forceUpdate/clock.vue @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/packages/inula-adapter/examples/$nextTick/nextTick.jsx b/packages/inula-adapter/examples/$nextTick/nextTick.jsx new file mode 100644 index 000000000..b7e781dde --- /dev/null +++ b/packages/inula-adapter/examples/$nextTick/nextTick.jsx @@ -0,0 +1,18 @@ +export default function (props) { + // inula rendering is synchronous, so this timeout appends this function at next asynchronous position to execute after render + useEffect(() => { + // this timeout should be wrapped as an adapter that way you can call only $nextClick( ... ) as usual + const timeout = setTimeout(() => { + // this must be wrapped, because at the time of render, this element does not exist yet + useInstance().$refs.inp.$el.attachEventListener('keyUp', e => { + console.log(e); + }); + }, 1); + + return () => { + clearTimeout(timeout); + }; + }); + + return ; +} diff --git a/packages/inula-adapter/examples/$nextTick/nextTick.vue b/packages/inula-adapter/examples/$nextTick/nextTick.vue new file mode 100644 index 000000000..13c348c00 --- /dev/null +++ b/packages/inula-adapter/examples/$nextTick/nextTick.vue @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/packages/inula-adapter/examples/$parent/list-item.jsx b/packages/inula-adapter/examples/$parent/list-item.jsx new file mode 100644 index 000000000..893ae9194 --- /dev/null +++ b/packages/inula-adapter/examples/$parent/list-item.jsx @@ -0,0 +1,25 @@ +import { useInstance } from 'vue-inula'; + +export default function (props) { + const { productId, productName, productType } = props; + + function filterType(type) { + useInstance().$parent.setFilterType(type); + } + + return ( + + {productId} + {productName} + + { + filterType(productType); + }} + > + {productType} + + + + ); +} diff --git a/packages/inula-adapter/examples/$parent/list-item.vue b/packages/inula-adapter/examples/$parent/list-item.vue new file mode 100644 index 000000000..5a415b888 --- /dev/null +++ b/packages/inula-adapter/examples/$parent/list-item.vue @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/packages/inula-adapter/examples/$parent/list.jsx b/packages/inula-adapter/examples/$parent/list.jsx new file mode 100644 index 000000000..7ce7b6c6b --- /dev/null +++ b/packages/inula-adapter/examples/$parent/list.jsx @@ -0,0 +1,27 @@ +import TableItem from 'table-item'; +import { expose } from 'vue-inula'; + +export default function (props) { + const { items } = props; + + const filteredIdems = items; + + function setFilterType(type) { + filteredItems = items.filter(item => item.type === type); + } + + expose(setFilterType); + + return ( + + + + + + + {filteredIdems.map(item => ( + + ))} +
IDProduct nameProduct type
+ ); +} diff --git a/packages/inula-adapter/examples/$parent/list.vue b/packages/inula-adapter/examples/$parent/list.vue new file mode 100644 index 000000000..6d51694f5 --- /dev/null +++ b/packages/inula-adapter/examples/$parent/list.vue @@ -0,0 +1,27 @@ + + \ No newline at end of file diff --git a/packages/inula-adapter/examples/$refs/refresh.jsx b/packages/inula-adapter/examples/$refs/refresh.jsx new file mode 100644 index 000000000..f6fe4b1e5 --- /dev/null +++ b/packages/inula-adapter/examples/$refs/refresh.jsx @@ -0,0 +1,24 @@ +import { useInstance } from 'vue-inula'; + +export default function (props) { + function validateAndSubmit() { + if (!useInstance().$refs[0].value) { + useInstance().$refs[0].focus(); + } else { + submit(); + } + } + + return ( +
+ + +
+ ); +} diff --git a/packages/inula-adapter/examples/$refs/refresh.vue b/packages/inula-adapter/examples/$refs/refresh.vue new file mode 100644 index 000000000..07238684a --- /dev/null +++ b/packages/inula-adapter/examples/$refs/refresh.vue @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/packages/inula-adapter/examples/$root/root.jsx b/packages/inula-adapter/examples/$root/root.jsx new file mode 100644 index 000000000..700753cf4 --- /dev/null +++ b/packages/inula-adapter/examples/$root/root.jsx @@ -0,0 +1,13 @@ +import { ref, defineExpose } from 'vue-inula'; + +export default function (props) { + const theme = ref('light'); + + function changeTheme(color) { + theme.value = color; + } + + defineExpose({ changeTheme }); + + return ; +} diff --git a/packages/inula-adapter/examples/$root/root.vue b/packages/inula-adapter/examples/$root/root.vue new file mode 100644 index 000000000..72a5ec176 --- /dev/null +++ b/packages/inula-adapter/examples/$root/root.vue @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/packages/inula-adapter/examples/$root/themeChanger.jsx b/packages/inula-adapter/examples/$root/themeChanger.jsx new file mode 100644 index 000000000..89bf7d11a --- /dev/null +++ b/packages/inula-adapter/examples/$root/themeChanger.jsx @@ -0,0 +1,19 @@ +import { useInstance } from 'vue-inula'; + +export default function (props) { + const { theme } = props; + + function changeTheme() { + useInstance().$root.changeTheme(theme); + } + + return ( + + ); +} diff --git a/packages/inula-adapter/examples/$root/themeChanger.vue b/packages/inula-adapter/examples/$root/themeChanger.vue new file mode 100644 index 000000000..abdfcabf7 --- /dev/null +++ b/packages/inula-adapter/examples/$root/themeChanger.vue @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/packages/inula-adapter/examples/$set/fruits.jsx b/packages/inula-adapter/examples/$set/fruits.jsx new file mode 100644 index 000000000..4b7bc20a4 --- /dev/null +++ b/packages/inula-adapter/examples/$set/fruits.jsx @@ -0,0 +1,60 @@ +import { reactive } from 'vue-inula'; + +export default function (props) { + const fruits = reactive([ + { name: 'apple', amount: 3 }, + { name: 'pear', amount: 0 }, + ]); + + function decrement(fruit) { + fruit.amount--; + } + function increment(fruit) { + fruit.amount++; + } + function addFruit() { + const fruitName = useInstance().$refs.newFruit.value; + this.$set(fruits, fruits.length, { name: fruitName, amount: 0 }); // how should we do this? Will use of reactive handle this case automaticaly? + useInstance().$refs.newFruit.value = ''; + } + + return ( +
+ +

+ + +

+
+ ); +} diff --git a/packages/inula-adapter/examples/$set/fruits.vue b/packages/inula-adapter/examples/$set/fruits.vue new file mode 100644 index 000000000..50488f709 --- /dev/null +++ b/packages/inula-adapter/examples/$set/fruits.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/packages/inula-adapter/examples/$t - plugins/App.jsx b/packages/inula-adapter/examples/$t - plugins/App.jsx new file mode 100644 index 000000000..238c7fdab --- /dev/null +++ b/packages/inula-adapter/examples/$t - plugins/App.jsx @@ -0,0 +1,7 @@ +import { useInstance } from 'vue-inula'; + +export default function (props) { + const introduction = useInstance().$t('introduction'); + + return
{introduction}
; +} diff --git a/packages/inula-adapter/examples/$t - plugins/App.vue b/packages/inula-adapter/examples/$t - plugins/App.vue new file mode 100644 index 000000000..cbd15cfc8 --- /dev/null +++ b/packages/inula-adapter/examples/$t - plugins/App.vue @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/packages/inula-adapter/examples/$t - plugins/i18n.js b/packages/inula-adapter/examples/$t - plugins/i18n.js new file mode 100644 index 000000000..b138b0bce --- /dev/null +++ b/packages/inula-adapter/examples/$t - plugins/i18n.js @@ -0,0 +1,18 @@ +import { createI18n } from 'vue-i18n'; + +const messages = { + en: { + introduction: 'Hello, I am Dora.', + }, + es: { + introduction: 'Ola, yo soy Dora.', + }, +}; + +const i18n = createI18n({ + locale: 'en', + fallbackLocale: 'en', + messages, +}); + +export default i18n; diff --git a/packages/inula-adapter/examples/$t - plugins/main.js b/packages/inula-adapter/examples/$t - plugins/main.js new file mode 100644 index 000000000..127eb9cac --- /dev/null +++ b/packages/inula-adapter/examples/$t - plugins/main.js @@ -0,0 +1,7 @@ +import { createApp } from 'vue-inula'; //here original function is replaced with adapter! +import App from 'App.vue'; +import i18n from './i18n'; + +const app = createApp(App); //createst global object +app.use(i18n); //attaches plugin to global object +app.mount('#app'); //renders and mounts root diff --git a/packages/inula-adapter/examples/custom functions/main.js b/packages/inula-adapter/examples/custom functions/main.js new file mode 100644 index 000000000..aac8dc5ea --- /dev/null +++ b/packages/inula-adapter/examples/custom functions/main.js @@ -0,0 +1,21 @@ +import store from './index'; + +let installed = false; + +function hideLoading(oMsg) { + store.commit(removeLoadingMsg, oMsg); +} + +export default { + install(app) { + if (installed) { + return; + } + installed = true; + // We need to decide how this can be processed to implement custom global functions + app.config.globalProperties.$hideLoading = hideLoading; + // Should we do something like this? + import { registerGlobal } from 'inula-vue'; + registerGlobal('$hideLoading', hideLoading); + }, +}; diff --git a/packages/inula-adapter/examples/emitter/emitter.jsx b/packages/inula-adapter/examples/emitter/emitter.jsx new file mode 100644 index 000000000..5dd300bee --- /dev/null +++ b/packages/inula-adapter/examples/emitter/emitter.jsx @@ -0,0 +1,16 @@ +export default function (props) { + function clicked() { + useInstance().$refs.emitter.$emit('eventEmitterClicked'); + } + + return ( + + ); +} diff --git a/packages/inula-adapter/examples/emitter/emitter.vue b/packages/inula-adapter/examples/emitter/emitter.vue new file mode 100644 index 000000000..004b7fbbc --- /dev/null +++ b/packages/inula-adapter/examples/emitter/emitter.vue @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/packages/inula-adapter/examples/emitter/listener.jsx b/packages/inula-adapter/examples/emitter/listener.jsx new file mode 100644 index 000000000..be6b52e29 --- /dev/null +++ b/packages/inula-adapter/examples/emitter/listener.jsx @@ -0,0 +1,5 @@ +export default function (props) { + useInstance().$refs.emitter.$on('eventEmitterClicked'); + + return emit event; +} diff --git a/packages/inula-adapter/examples/emitter/listener.vue b/packages/inula-adapter/examples/emitter/listener.vue new file mode 100644 index 000000000..d57c212d7 --- /dev/null +++ b/packages/inula-adapter/examples/emitter/listener.vue @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/packages/inula-adapter/npm/pinia/package.json b/packages/inula-adapter/npm/pinia/package.json new file mode 100644 index 000000000..0888b15d6 --- /dev/null +++ b/packages/inula-adapter/npm/pinia/package.json @@ -0,0 +1,5 @@ +{ + "module": "./esm/pinia-adapter.js", + "main": "./cjs/pinia-adapter.js", + "types": "./@types/index.d.ts" +} diff --git a/packages/inula-adapter/npm/vuex/package.json b/packages/inula-adapter/npm/vuex/package.json new file mode 100644 index 000000000..bf4972656 --- /dev/null +++ b/packages/inula-adapter/npm/vuex/package.json @@ -0,0 +1,5 @@ +{ + "module": "./esm/vuex-adapter.js", + "main": "./cjs/vuex-adapter.js", + "types": "./@types/index.d.ts" +} diff --git a/packages/inula-adapter/package.json b/packages/inula-adapter/package.json new file mode 100644 index 000000000..09bdb70dc --- /dev/null +++ b/packages/inula-adapter/package.json @@ -0,0 +1,63 @@ +{ + "name": "inula-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": [ + "/vue", + "/pinia", + "/vuex", + "README.md" + ], + "scripts": { + "test-ui": "vitest --ui", + "test": "vitest", + "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": "^0.1.14" + }, + "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", + "@testing-library/user-event": "^12.1.10", + "@types/jest": "^29.5.14", + "@vitejs/plugin-react": "^4.2.1", + "@vitest/ui": "^0.34.5", + "jsdom": "^24.0.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": "^0.34.5" + } +} diff --git a/packages/inula-adapter/scripts/build-types.js b/packages/inula-adapter/scripts/build-types.js new file mode 100644 index 000000000..dbc9aa210 --- /dev/null +++ b/packages/inula-adapter/scripts/build-types.js @@ -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, { recursive: true }); + } 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')]; diff --git a/packages/inula-adapter/scripts/rollup.config.js b/packages/inula-adapter/scripts/rollup.config.js new file mode 100644 index 000000000..eb483d36c --- /dev/null +++ b/packages/inula-adapter/scripts/rollup.config.js @@ -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'), +]; diff --git a/packages/inula-adapter/src/pinia/index.ts b/packages/inula-adapter/src/pinia/index.ts new file mode 100644 index 000000000..74e0da324 --- /dev/null +++ b/packages/inula-adapter/src/pinia/index.ts @@ -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 './pinia'; diff --git a/packages/inula-adapter/src/pinia/pinia.ts b/packages/inula-adapter/src/pinia/pinia.ts new file mode 100644 index 000000000..8a4c209b5 --- /dev/null +++ b/packages/inula-adapter/src/pinia/pinia.ts @@ -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(); + +export function defineStore< + Id extends string, + S extends Record, + A extends Record, + C extends Record, +>(definition: StoreDefinition): (pinia?: any) => Store; + +export function defineStore< + Id extends string, + S extends Record, + A extends Record, + C extends Record, +>(id: Id, definition: Omit, 'id'>): (pinia?: any) => Store; + +export function defineStore>( + id: Id, + setup: StoreSetup +): (pinia?: any) => Store, FilterAction, FilterComputed>; + +export function defineStore(idOrDef: any, setupOrDef?: any) { + let id: string; + let definition: StoreDefinition | StoreSetup; + let isSetup = false; + + if (typeof idOrDef === 'string') { + isSetup = typeof setupOrDef === 'function'; + id = idOrDef; + definition = setupOrDef; + } else { + id = idOrDef.id; + definition = idOrDef; + } + + if (isSetup) { + return defineSetupStore(id, definition as StoreSetup); + } else { + return defineOptionsStore(id, definition as StoreDefinition); + } +} + +/** + * createStore实现中会给actions增加第一个参数store,pinia不需要,所以需要去掉 + * @param actions + */ +function enhanceActions( + actions?: ActionType, Record, Record> +) { + if (!actions) { + return {}; + } + + return Object.fromEntries( + Object.entries(actions).map(([key, value]) => { + return [ + key, + function (this: StoreObj, state: Record, ...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>(id: string, storeSetup: StoreSetup) { + return () => { + const data = storeSetup(); + if (!data) { + return {}; + } + + if (storeMap.has(id)) { + return storeMap.get(id)!(); + } + + const state: Record = {}; + const actions: Record = {}; + const getters: Record = {}; + 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).getter as AnyFunction; + } + } + + const useStore = createStore({ + id, + state, + computed: getters, + actions: enhanceActions(actions), + }); + + storeMap.set(id, useStore); + + return useStore(); + }; +} + +export function mapStores< + S extends Record, + A extends Record, + C extends Record, +>(...stores: (() => Store)[]): { [key: string]: () => Store } { + const result: { [key: string]: () => Store } = {}; + + stores.forEach((store: () => Store) => { + const expandedStore = store(); + result[`${expandedStore.id}Store`] = () => expandedStore; + }); + + return result; +} + +export function storeToRefs< + S extends Record, + A extends Record, + C extends Record, +>(store: Store): StoreToRefsReturn { + 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; +} + +export function createPinia() { + console.warn( + `The pinia-adapter in inula does not support the createPinia interface. Please modify your code accordingly.` + ); + + const result = { + install: (app: any) => {}, + use: (plugin: any) => result, + state: {}, + }; + + return result; +} diff --git a/packages/inula-adapter/src/pinia/types.ts b/packages/inula-adapter/src/pinia/types.ts new file mode 100644 index 000000000..cf98faec9 --- /dev/null +++ b/packages/inula-adapter/src/pinia/types.ts @@ -0,0 +1,103 @@ +/* + * 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, UnwrapRef, ComputedImpl } from 'openinula'; + +export type StoreSetup> = () => R; + +export type AnyFunction = (...args: any[]) => any; + +// defineStore init type +export interface StoreDefinition< + Id extends string = string, + S extends Record = Record, + A extends Record = Record, + C extends Record = Record, +> { + id?: Id; + state?: () => S; + actions?: ActionType; + getters?: ComputedType; +} + +// defineStore return type +export type Store< + S extends Record, + A extends Record, + C extends Record, +> = { + $s: S; + $state: S; + $a: ActionType; + $c: ComputedType; + $subscribe: (listener: Listener) => void; + $unsubscribe: (listener: Listener) => void; +} & { [K in keyof S]: S[K] } & { [K in keyof ActionType]: ActionType[K] } & { + [K in keyof ComputedType]: ReturnType[K]>; +}; + +export type ActionType = A & ThisType & WithGetters>; + +type ComputedType = { + [K in keyof C]: AddFirstArg; +} & ThisType & WithGetters>; +type AddFirstArg = 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 = { + readonly [k in keyof G]: G[k] extends (...args: any[]) => infer R ? R : UnwrapRef; +}; + +type Listener = (change: any) => void; + +// Filter state properties +export type FilterState> = { + [K in FilterStateProperties]: UnwrapRef; +}; +type FilterStateProperties> = { + [K in keyof T]: T[K] extends ComputedImpl + ? never + : T[K] extends RefType + ? K + : T[K] extends Record // Reactive类型 + ? K + : never; +}[keyof T]; + +// Filter action properties +export type FilterAction> = { + [K in FilterFunctionProperties]: T[K] extends AnyFunction ? T[K] : never; +}; +type FilterFunctionProperties> = { + [K in keyof T]: T[K] extends AnyFunction ? K : never; +}[keyof T]; + +// Filter computed properties +export type FilterComputed> = { + [K in FilterComputedProperties]: T[K] extends ComputedImpl ? (T extends AnyFunction ? T : never) : never; +}; +type FilterComputedProperties> = { + [K in keyof T]: T[K] extends ComputedImpl ? K : never; +}[keyof T]; + +export type StoreToRefsReturn, C extends Record> = { + [K in keyof S]: RefType; +} & { + [K in keyof ComputedType]: Readonly[K]>>>; +}; diff --git a/packages/inula-adapter/src/vue/Teleport.ts b/packages/inula-adapter/src/vue/Teleport.ts new file mode 100644 index 000000000..502d876cb --- /dev/null +++ b/packages/inula-adapter/src/vue/Teleport.ts @@ -0,0 +1,16 @@ +import { useState, useEffect, createPortal } from 'openinula'; +export function Teleport({ to, children }) { + const container = useState(() => { + document.createElement('div'); + }); + + useEffect(() => { + to.appendChild(container); + + return () => { + to.removeChild(container); + }; + }, [to, container]); + + return createPortal(children, container); +} diff --git a/packages/inula-adapter/src/vue/compare.ts b/packages/inula-adapter/src/vue/compare.ts new file mode 100644 index 000000000..0008c5eaa --- /dev/null +++ b/packages/inula-adapter/src/vue/compare.ts @@ -0,0 +1,54 @@ +/* + * 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. + */ + +/** + * 兼容IE浏览器没有Object.is + */ +export function isSame(x: any, y: any) { + if (!(typeof Object.is === 'function')) { + if (x === y) { + // +0 != -0 + return x !== 0 || 1 / x === 1 / y; + } else { + // NaN == NaN + return x !== x && y !== y; + } + } else { + return Object.is(x, y); + } +} + +export function shallowCompare(paramX: any, paramY: any): boolean { + if (isSame(paramX, paramY)) { + return true; + } + + // 对比对象 + if (typeof paramX === 'object' && typeof paramY === 'object' && paramX !== null && paramY !== null) { + const keysX = Object.keys(paramX); + const keysY = Object.keys(paramY); + + // key长度不相等时直接返回不相等 + if (keysX.length !== keysY.length) { + return false; + } + + return keysX.every( + (key, i) => Object.prototype.hasOwnProperty.call(paramY, key) && isSame(paramX[key], paramY[keysX[i]]) + ); + } + + return false; +} diff --git a/packages/inula-adapter/src/vue/condition.tsx b/packages/inula-adapter/src/vue/condition.tsx new file mode 100644 index 000000000..06df086c3 --- /dev/null +++ b/packages/inula-adapter/src/vue/condition.tsx @@ -0,0 +1,52 @@ +/* + * 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 Inula, { isValidElement, Children, FC } from 'openinula'; + +interface ConditionalProps { + children?: any; + condition: boolean; +} + +export const If: FC = ({ children, condition }) => { + return condition ? <>{children} : null; +}; + +export const ElseIf: FC = If; + +export const Else: FC> = ({ children }) => { + return <>{children}; +}; + +interface ConditionalRendererProps { + children?: any; +} + +export const ConditionalRenderer: FC = ({ children }) => { + const childrenArray = Children.toArray(children); + const renderedChild = childrenArray.find(child => { + if (isValidElement(child)) { + if (child.type === If || child.type === ElseIf) { + return child.props.condition; + } + if (child.type === Else) { + return true; + } + } + return false; + }); + + return renderedChild ? <>{renderedChild} : null; +}; diff --git a/packages/inula-adapter/src/vue/directive.tsx b/packages/inula-adapter/src/vue/directive.tsx new file mode 100644 index 000000000..1b651952a --- /dev/null +++ b/packages/inula-adapter/src/vue/directive.tsx @@ -0,0 +1,116 @@ +import { ComponentType, useCallback, useEffect, useLayoutEffect, useMemo, createElement, vueReactive } from 'openinula'; +import { useDirectives } from './globalAPI'; + +const { useInstance } = vueReactive; + +/** + * Vue写法: