From 85e20114cac0a6e87b9b9b0cae4e6dbe3e4d9b80 Mon Sep 17 00:00:00 2001 From: HoikanChen Date: Wed, 11 Oct 2023 14:32:44 +0800 Subject: [PATCH 001/108] =?UTF-8?q?refactor:=E4=BC=98=E5=8C=96=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E7=BB=84=E4=BB=B6=E6=B8=B2=E6=9F=93=E5=8F=91=E7=94=9F?= =?UTF-8?q?setState=E4=B8=8D=E8=A7=A6=E5=8F=91=E6=95=B4=E4=B8=AA=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E9=87=8D=E6=96=B0=E6=9B=B4=E6=96=B0=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E7=BB=84=E4=BB=B6=E6=9C=AC=E8=BA=AB=E9=A9=AC=E4=B8=8A?= =?UTF-8?q?=E9=87=8D=E8=BF=90=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ComponentTest/FunctionComponent.test.js | 36 +++++++++++++++++++ packages/inula/src/renderer/hooks/HookMain.ts | 34 +++++++++++++++++- .../src/renderer/hooks/UseReducerHook.ts | 10 ++++-- 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/packages/inula/scripts/__tests__/ComponentTest/FunctionComponent.test.js b/packages/inula/scripts/__tests__/ComponentTest/FunctionComponent.test.js index 0b5d18a8..ef17a0aa 100644 --- a/packages/inula/scripts/__tests__/ComponentTest/FunctionComponent.test.js +++ b/packages/inula/scripts/__tests__/ComponentTest/FunctionComponent.test.js @@ -14,6 +14,7 @@ */ import * as Inula from '../../../src/index'; +const { useState } = Inula; describe('FunctionComponent Test', () => { it('渲染无状态组件', () => { const App = props => { @@ -89,4 +90,39 @@ describe('FunctionComponent Test', () => { Inula.render(, container); expect(container.querySelector('div').style['_values']['--max-segment-num']).toBe(10); }); + + it('函数组件渲染中重新发生setState不触发整个应用重新更新', () => { + function CountLabel({ count }) { + const [prevCount, setPrevCount] = useState(count); + const [trend, setTrend] = useState(null); + if (prevCount !== count) { + setPrevCount(count); + setTrend(count > prevCount ? 'increasing' : 'decreasing'); + } + return ( + <> +

{count}

+ + {trend &&

The count is {trend}

} + + ); + } + + let count = 0; + function Child({ trend }) { + count++; + return
{trend}
; + } + + let update; + function App() { + const [count, setCount] = useState(0); + update = setCount; + return ; + } + + Inula.render(, container); + update(1); + expect(count).toBe(2); + }) }); diff --git a/packages/inula/src/renderer/hooks/HookMain.ts b/packages/inula/src/renderer/hooks/HookMain.ts index 48f39f42..c2943f49 100644 --- a/packages/inula/src/renderer/hooks/HookMain.ts +++ b/packages/inula/src/renderer/hooks/HookMain.ts @@ -18,12 +18,19 @@ import type { VNode } from '../Types'; import { getLastTimeHook, setLastTimeHook, setCurrentHook, getNextHook } from './BaseHook'; import { HookStage, setHookStage } from './HookStage'; +const NESTED_UPDATE_LIMIT = 50; +// state updated in render phrase +let hasUpdatedInRender = false; function resetGlobalVariable() { setHookStage(null); setLastTimeHook(null); setCurrentHook(null); } +export function markUpdatedInRender() { + hasUpdatedInRender = true; +} + // hook对外入口 export function runFunctionWithHooks, Arg>( funcComp: (props: Props, arg: Arg) => any, @@ -45,8 +52,14 @@ export function runFunctionWithHooks, Arg>( setHookStage(HookStage.Update); } - const comp = funcComp(props, arg); + let comp = funcComp(props, arg); + if (hasUpdatedInRender) { + resetGlobalVariable(); + processing.oldHooks = processing.hooks; + setHookStage(HookStage.Update); + comp = runFunctionAgain(funcComp, props, arg); + } // 设置hook阶段为null,用于判断hook是否在函数组件中调用 setHookStage(null); @@ -63,3 +76,22 @@ export function runFunctionWithHooks, Arg>( return comp; } + +function runFunctionAgain, Arg>( + funcComp: (props: Props, arg: Arg) => any, + props: Props, + arg: Arg +) { + let reRenderTimes = 0; + let childElements; + while (hasUpdatedInRender) { + reRenderTimes++; + if (reRenderTimes > NESTED_UPDATE_LIMIT) { + throw new Error('Too many setState called in function component'); + } + hasUpdatedInRender = false; + childElements = funcComp(props, arg); + } + + return childElements; +} diff --git a/packages/inula/src/renderer/hooks/UseReducerHook.ts b/packages/inula/src/renderer/hooks/UseReducerHook.ts index 1278e65e..fe56820b 100644 --- a/packages/inula/src/renderer/hooks/UseReducerHook.ts +++ b/packages/inula/src/renderer/hooks/UseReducerHook.ts @@ -21,6 +21,7 @@ import { setStateChange } from '../render/FunctionComponent'; import { getHookStage, HookStage } from './HookStage'; import type { VNode } from '../Types'; import { getProcessingVNode } from '../GlobalVar'; +import { markUpdatedInRender } from "./HookMain"; // 构造新的Update数组 function insertUpdate(action: A, hook: Hook): Update { @@ -64,8 +65,13 @@ export function TriggerAction(vNode: VNode, hook: Hook, isUseState: } } - // 执行vNode节点渲染 - launchUpdateFromVNode(vNode); + if (vNode === getProcessingVNode()) { + // 绑定的VNode就是当前渲染的VNode时,就是在函数组件体内触发setState + markUpdatedInRender(); + } else { + // 执行vNode节点渲染 + launchUpdateFromVNode(vNode); + } } export function useReducerForInit(reducer, initArg, init, isUseState?: boolean): [S, Trigger] { From 1bb0478883255d4d7b7ed99f425970a93d7800ff Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Thu, 12 Oct 2023 15:56:45 +0800 Subject: [PATCH 002/108] =?UTF-8?q?[inula]=20=E5=A4=A7=E6=95=B0?= =?UTF-8?q?=E7=BB=84=E5=90=88=E5=B9=B6=E4=BD=BF=E7=94=A8=20concat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/src/renderer/TreeBuilder.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/inula/src/renderer/TreeBuilder.ts b/packages/inula/src/renderer/TreeBuilder.ts index dff56e07..50326e25 100644 --- a/packages/inula/src/renderer/TreeBuilder.ts +++ b/packages/inula/src/renderer/TreeBuilder.ts @@ -55,6 +55,9 @@ import { getPathArr } from './utils/vNodePath'; import { injectUpdater } from '../external/devtools'; import { popCurrentRoot, pushCurrentRoot } from './RootStack'; +// 使用 push 扩展语法合并数组场景下被合并数组元素的上限(经验值) +const MAX_NUM_PUSH_MERGE_ARRAY = 1000; + // 不可恢复错误 let unrecoverableErrorDuringBuild: any = null; @@ -81,7 +84,12 @@ function collectDirtyNodes(vNode: VNode, parent: VNode): void { if (parent.dirtyNodes === null) { parent.dirtyNodes = dirtyNodes; } else { - parent.dirtyNodes.push(...vNode.dirtyNodes!); + // 超过上限继续使用 push 方法合并数组将导致性能劣化/调用栈溢出 + if (dirtyNodes.length > MAX_NUM_PUSH_MERGE_ARRAY) { + parent.dirtyNodes = parent.dirtyNodes.concat(dirtyNodes); + } else { + parent.dirtyNodes.push(...dirtyNodes); + } dirtyNodes.length = 0; } vNode.dirtyNodes = null; From f43c6dd7c6a7bf489bef48674eb74485f6aac0e5 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Thu, 12 Oct 2023 16:11:27 +0800 Subject: [PATCH 003/108] =?UTF-8?q?[inula]=20=E8=A7=A3=E5=86=B3=20?= =?UTF-8?q?mouseover=20=E9=87=8D=E5=A4=8D=E8=A7=A6=E5=8F=91=20mouseEnter?= =?UTF-8?q?=20=E4=BA=8B=E4=BB=B6=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/src/event/InulaEventMain.ts | 2 +- packages/inula/src/event/MouseEvent.ts | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/inula/src/event/InulaEventMain.ts b/packages/inula/src/event/InulaEventMain.ts index a281acf1..cff523fc 100644 --- a/packages/inula/src/event/InulaEventMain.ts +++ b/packages/inula/src/event/InulaEventMain.ts @@ -146,7 +146,7 @@ function triggerInulaEvents( let mouseEnterListeners: ListenerUnitList = []; if (inulaEventToNativeMap.get('onMouseEnter')!.includes(nativeEvtName)) { - mouseEnterListeners = getMouseEnterListeners(nativeEvtName, vNode, nativeEvent, target); + mouseEnterListeners = getMouseEnterListeners(nativeEvtName, vNode, nativeEvent as MouseEvent, target); } let changeEvents: ListenerUnitList = []; diff --git a/packages/inula/src/event/MouseEvent.ts b/packages/inula/src/event/MouseEvent.ts index b817db36..7f514b83 100644 --- a/packages/inula/src/event/MouseEvent.ts +++ b/packages/inula/src/event/MouseEvent.ts @@ -13,7 +13,7 @@ * See the Mulan PSL v2 for more details. */ -import { getNearestVNode } from '../dom/DOMInternalKeys'; +import { getNearestVNode, getVNode } from '../dom/DOMInternalKeys'; import { WrappedEvent } from './EventWrapper'; import { VNode } from '../renderer/vnode/VNode'; import { AnyNativeEvent, ListenerUnitList } from './Types'; @@ -87,12 +87,29 @@ function getEndpointVNode( return [fromVNode, toVNode]; } +function checkIsInulaNode(related: HTMLElement): boolean { + if (getVNode(related) || getNearestVNode(related)) { + return true; + } + return false; +} + export function getMouseEnterListeners( domEventName: string, targetInst: null | VNode, - nativeEvent: AnyNativeEvent, + nativeEvent: MouseEvent, nativeEventTarget: null | EventTarget ): ListenerUnitList { + + if (domEventName === 'mouseover') { + // 如果 related 节点是 openInula 框架管理的,那么在 out 事件节点已经触发过 mouseEnter 或者 mouseLeave 事件了,不需要 over 事件再次触发 + // IE 通过 fromElement 属性获取失去焦点的 DOM 节点 + const related = nativeEvent.relatedTarget || (nativeEvent as any).fromElemnt; + if (related && checkIsInulaNode(related)) { + return []; + } + } + // 获取起点和终点的VNode const [fromVNode, toVNode] = getEndpointVNode(domEventName, targetInst, nativeEvent); if (fromVNode === toVNode) { From 3c776ec701aa09e71e6db176553eec0df2e3742c Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Thu, 12 Oct 2023 16:16:58 +0800 Subject: [PATCH 004/108] =?UTF-8?q?[inula]=20=E8=A7=A3=E5=86=B3?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E5=8D=B8=E8=BD=BD=E5=A4=B1=E8=B4=A5=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/src/dom/DOMExternal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/inula/src/dom/DOMExternal.ts b/packages/inula/src/dom/DOMExternal.ts index 001d18f5..076709e9 100644 --- a/packages/inula/src/dom/DOMExternal.ts +++ b/packages/inula/src/dom/DOMExternal.ts @@ -89,7 +89,7 @@ function findDOMNode(domOrEle?: Element): null | Element | Text { // 情况根节点监听器 function removeRootEventLister(container: Container) { - const events = (container._treeRoot as any).$EV; + const events = (container as any).$EV; if (events) { Object.keys(events).forEach(event => { const listener = events[event]; From e649ba5d55a7c3e17d8117927e1f466513fd66e5 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Thu, 12 Oct 2023 16:24:37 +0800 Subject: [PATCH 005/108] =?UTF-8?q?[all]=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=86=85=E5=AE=B9=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- RELEASE-NOTES.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index a3f44bd7..f2eb7051 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,3 +1,28 @@ +# 0.0.2 版本 + +## 新特性 + +- **inula-request** 新增响应体中获取完整 URL 能力。 + +## API变更 + +无 + +## Bug修复 + +- **inula** 解决事件卸载失败问题。 +- **inula** 解决 mouseover 重复触发 mouseEnter 事件问题。 +- **inula** 大数组合并使用 concat。 +- **inula** 事件支持 defaultPrevented 属性 + +## CVE漏洞修复 + +无 + +## 已知问题 + +无 + # 0.0.1 版本 ## 新特性 From 96835ad1a3230029c54b8334c54fd9b3b21b9586 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Fri, 13 Oct 2023 15:14:14 +0800 Subject: [PATCH 006/108] [inula-dev-tools] inula-dev-tools project init --- packages/inula-dev-tools/README.md | 0 packages/inula-dev-tools/babel.config.js | 41 ++++++++ packages/inula-dev-tools/externals.d.ts | 19 ++++ packages/inula-dev-tools/global.d.ts | 21 ++++ packages/inula-dev-tools/package.json | 54 ++++++++++ packages/inula-dev-tools/src/manifest.json | 35 +++++++ packages/inula-dev-tools/tsconfig.json | 22 ++++ packages/inula-dev-tools/webpack.config.js | 114 +++++++++++++++++++++ packages/inula-dev-tools/webpack.dev.js | 77 ++++++++++++++ 9 files changed, 383 insertions(+) create mode 100644 packages/inula-dev-tools/README.md create mode 100644 packages/inula-dev-tools/babel.config.js create mode 100644 packages/inula-dev-tools/externals.d.ts create mode 100644 packages/inula-dev-tools/global.d.ts create mode 100644 packages/inula-dev-tools/package.json create mode 100644 packages/inula-dev-tools/src/manifest.json create mode 100644 packages/inula-dev-tools/tsconfig.json create mode 100644 packages/inula-dev-tools/webpack.config.js create mode 100644 packages/inula-dev-tools/webpack.dev.js diff --git a/packages/inula-dev-tools/README.md b/packages/inula-dev-tools/README.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/inula-dev-tools/babel.config.js b/packages/inula-dev-tools/babel.config.js new file mode 100644 index 00000000..10a19b67 --- /dev/null +++ b/packages/inula-dev-tools/babel.config.js @@ -0,0 +1,41 @@ +/* + * 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 = api => { + const isTest = api.env('test'); + console.log('isTest', isTest); + + const plugins = [ + ['@babel/plugin-proposal-class-properties', { loose: false }], + ]; + + if (process.env.NODE_ENV !== 'production') { + plugins.push(['@babel/plugin-transform-react-jsx-source']); + } + + return { + presets: [ + '@babel/preset-env', + '@babel/preset-typescript', + [ + '@babel/preset-react', { + runtime: 'classic', + 'pragma': 'Inula.createElement', + 'pragmaFrag': 'Inula.Fragment', + }] + ], + plugins, + }; +}; diff --git a/packages/inula-dev-tools/externals.d.ts b/packages/inula-dev-tools/externals.d.ts new file mode 100644 index 00000000..72bdaf9d --- /dev/null +++ b/packages/inula-dev-tools/externals.d.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +declare module '*.less' { + const resource: {[key: string]: string}; + export = resource; +} diff --git a/packages/inula-dev-tools/global.d.ts b/packages/inula-dev-tools/global.d.ts new file mode 100644 index 00000000..646c0399 --- /dev/null +++ b/packages/inula-dev-tools/global.d.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/* + 区分是否开发者模式 + */ +declare var isDev: boolean; +declare var isTest: boolean; +declare const __VERSION__: string; diff --git a/packages/inula-dev-tools/package.json b/packages/inula-dev-tools/package.json new file mode 100644 index 00000000..3675f56d --- /dev/null +++ b/packages/inula-dev-tools/package.json @@ -0,0 +1,54 @@ +{ + "name": "inula-dev-tools", + "version": "0.0.1", + "description": "Inula chrome dev extension", + "main": "", + "scripts": { + "build": "webpack --config ./webpack.config.js", + "watch": "webpack --config ./webpack.config.js --watch", + "build-dev": "webpack --config ./webpack.dev.js", + "start": "npm run build && webpack serve --config ./webpack.dev.js", + "test": "jest" + }, + "keywords": ["inula-dev-tools"], + "license": "MulanPSL2", + "devDependencies": { + "@babel/core": "^7.12.3", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-transform-react-jsx-source": "^7.18.6", + "@babel/preset-env": "^7.21.1", + "@babel/preset-react": "^7.12.1", + "@babel/preset-typescript": "^7.16.7", + "@types/chrome": "0.0.190", + "@types/jest": "27.4.1", + "@typescript-eslint/eslint-plugin": "4.8.0", + "@typescript-eslint/parser": "4.8.0", + "babel-jest": "^27.5.1", + "eslint": "7.13.0", + "eslint-config-prettier": "^6.9.0", + "eslint-plugin-jest": "^22.15.0", + "eslint-plugin-no-for-of-loops": "^1.0.0", + "eslint-plugin-no-function-declare-after-return": "^1.0.0", + "eslint-plugin-react": "7.14.3", + "babel-loader": "8.1.0", + "css-loader": "^6.7.1", + "html-webpack-plugin": "5.5.0", + "jest": "27.5.1", + "less": "4.1.2", + "less-loader": "10.2.0", + "style-loader": "^3.3.1", + "ts-jest": "27.1.4", + "ts-loader": "^9.3.1", + "typescript": "4.2.3", + "webpack": "5.70.0", + "webpack-cli": "4.9.2", + "webpack-dev-server": "^4.7.4" + }, + "dependencies": { + "openinula": "^0.0.1", + "flatted-object": "^0.1.2", + "json-decycle": "^2.0.1", + "lodash": "^4.17.21", + "object-assign": "^4.1.1" + } +} diff --git a/packages/inula-dev-tools/src/manifest.json b/packages/inula-dev-tools/src/manifest.json new file mode 100644 index 00000000..34c0f2b6 --- /dev/null +++ b/packages/inula-dev-tools/src/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "Inula dev tools", + "description": "Inula chrome dev extension", + "version": "1.0", + "minimum_chrome_version": "10.0", + "manifest_version": 3, + "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", + "background": { + "script": [ + "background.js" + ], + "persistent": true + }, + "permissions": [ + "file:///*", + "http://*/*", + "https://*/*" + ], + "devtools_page": "main.html", + "content_scripts": [ + { + "matches": [ + "" + ], + "js": [ + "contentScript.js" + ], + "run_at": "document_start" + } + ], + "web_accessible_resources": [ + "injector.js", + "background.js" + ] +} diff --git a/packages/inula-dev-tools/tsconfig.json b/packages/inula-dev-tools/tsconfig.json new file mode 100644 index 00000000..2e1cc0d6 --- /dev/null +++ b/packages/inula-dev-tools/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "incremental": false, + "allowJs": true, + "strict": true, + "noImplicitAny": false, + "module": "es6", + "moduleResolution": "node", + "target": "es5", + "jsx": "preserve", + "allowSyntheticDefaultImports": true, + "lib": ["dom", "esnext", "ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020"], + "baseUrl": "./", + "paths": { + "*": ["types/*"] + } + }, + "include": [ + "./src/*/*.ts", "./src/index.d.ts", "./src/*/*.tsx" + ] +} diff --git a/packages/inula-dev-tools/webpack.config.js b/packages/inula-dev-tools/webpack.config.js new file mode 100644 index 00000000..0e23a1b0 --- /dev/null +++ b/packages/inula-dev-tools/webpack.config.js @@ -0,0 +1,114 @@ +/* + * 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 webpack from 'webpack'; +import fs from 'fs'; + +function handleBuildDir() { + const staticDir = path.join(__dirname, 'build'); + console.log('staticDir: ', staticDir); + const isBuildExist = fs.existsSync(staticDir); + console.log('isBuildExist: ', isBuildExist); + + if (!isBuildExist) { + fs.mkdirSync(staticDir); + } + fs.copyFileSync( + path.join(__dirname, 'src', 'panel', 'panel.html'), + path.join(staticDir, 'panel.html') + ); + fs.copyFileSync( + path.join(__dirname, 'src', 'panelX', 'panel.html'), + path.join(staticDir, 'panelX.html') + ); + fs.copyFileSync( + path.join(__dirname, 'src', 'main', 'main.html'), + path.join(staticDir, 'main.html') + ); + fs.copyFileSync( + path.join(__dirname, 'src', 'manifest.json'), + path.join(staticDir, 'manifest.json') + ); + fs.copyFileSync( + path.join( + __dirname, + '../inula/build/umd', + 'inula.development.js' + ), + path.join(staticDir, 'inula.development.js') + ); +} + +handleBuildDir(); + +const config = { + entry: { + background: './src/background/index.ts', + main: './src/main/index.ts', + injector: './src/injector/index.ts', + contentScript: './sec/contentScript/index.ts', + panel: './src/panel/index.ts', + panelX: './src/panelX/index.ts', + }, + output: { + path: path.resolve(__dirname, './build'), + filename: '[name].js' + }, + mode: 'development', + devtool: 'inline-source-map', + module: { + rules: [ + { + test: /(\.ts)|(\.tsx)$/, + exclude: function (path) { + return /node_modules/.test(path) && !/inula/.test(path); + }, + use: [ + { + loader: 'babel-loader', + }, + ], + }, + { + test: /\.less/i, + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + modules: true, + }, + }, + 'less-loader', + ], + }, + ], + }, + resolve: { + extensions: ['.js', '.ts', 'tsx'], + }, + externals: { + openinula: 'Inula', + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': '"development"', + isDev: 'false', + }), + ], +}; + +module.exports = config; diff --git a/packages/inula-dev-tools/webpack.dev.js b/packages/inula-dev-tools/webpack.dev.js new file mode 100644 index 00000000..99cc835a --- /dev/null +++ b/packages/inula-dev-tools/webpack.dev.js @@ -0,0 +1,77 @@ +/* + * 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 webpack from 'webpack'; + +// 用于 panel 页面开发 +module.exports = { + entry: { + panel: './src/panel/index.tsx', + mockPage: './src/devtools/mockPage/index.tsx', + }, + output: { + path: path.resolve(__dirname, './dist'), + filename: '[name].js' + }, + mode: 'development', + devtool: 'source-map', + module: { + rules: [ + { + test: /\.tsx?$/, + exclude: /node_modules/, + use: [ + { + loader: 'babel-loader', + }, + ], + }, + { + test: /\.less/i, + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + modules: true, + }, + }, + 'less-loader', + ], + }, + ], + }, + resolve: { + extensions: ['.js', '.ts', 'tsx'], + }, + externals: { + openinula: 'Inula', + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': '"development"', + isDev: 'true', + }), + ], + devServer: { + static: { + directory: path.join(__dirname, 'build'), + }, + open: 'panel.html', + port: 9000, + magicHtml: true, + } +}; From e6e226d053a7bdf5db0b565b45a56a9200e3bc9f Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Fri, 13 Oct 2023 16:50:57 +0800 Subject: [PATCH 007/108] =?UTF-8?q?[inula-dev-tools]=20=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=B1=BB=E5=87=BD=E6=95=B0=E5=90=88=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-dev-tools/src/background/index.ts | 0 .../inula-dev-tools/src/utils/Checkbox.tsx | 43 ++++++++++++ .../inula-dev-tools/src/utils/PickElement.tsx | 21 ++++++ .../inula-dev-tools/src/utils/ViewSource.tsx | 21 ++++++ .../inula-dev-tools/src/utils/constants.ts | 66 +++++++++++++++++++ .../inula-dev-tools/src/utils/injectUtils.ts | 47 +++++++++++++ packages/inula-dev-tools/src/utils/logUtil.ts | 35 ++++++++++ .../inula-dev-tools/src/utils/publicUtil.ts | 20 ++++++ .../inula-dev-tools/src/utils/regExpUtil.ts | 29 ++++++++ .../src/utils/transferUtils.ts | 47 +++++++++++++ 10 files changed, 329 insertions(+) create mode 100644 packages/inula-dev-tools/src/background/index.ts create mode 100644 packages/inula-dev-tools/src/utils/Checkbox.tsx create mode 100644 packages/inula-dev-tools/src/utils/PickElement.tsx create mode 100644 packages/inula-dev-tools/src/utils/ViewSource.tsx create mode 100644 packages/inula-dev-tools/src/utils/constants.ts create mode 100644 packages/inula-dev-tools/src/utils/injectUtils.ts create mode 100644 packages/inula-dev-tools/src/utils/logUtil.ts create mode 100644 packages/inula-dev-tools/src/utils/publicUtil.ts create mode 100644 packages/inula-dev-tools/src/utils/regExpUtil.ts create mode 100644 packages/inula-dev-tools/src/utils/transferUtils.ts diff --git a/packages/inula-dev-tools/src/background/index.ts b/packages/inula-dev-tools/src/background/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/inula-dev-tools/src/utils/Checkbox.tsx b/packages/inula-dev-tools/src/utils/Checkbox.tsx new file mode 100644 index 00000000..f29f6e8b --- /dev/null +++ b/packages/inula-dev-tools/src/utils/Checkbox.tsx @@ -0,0 +1,43 @@ +/* + * 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. + */ + +export function Checkbox({ value }) { + return ( +
+
+
+ ); +} diff --git a/packages/inula-dev-tools/src/utils/PickElement.tsx b/packages/inula-dev-tools/src/utils/PickElement.tsx new file mode 100644 index 00000000..80ed91d5 --- /dev/null +++ b/packages/inula-dev-tools/src/utils/PickElement.tsx @@ -0,0 +1,21 @@ +/* + * 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 { createContext } from "openinula"; + +const PickElementContext = createContext(null); +PickElementContext.displayName = 'PickElementContext'; + +export default PickElementContext; diff --git a/packages/inula-dev-tools/src/utils/ViewSource.tsx b/packages/inula-dev-tools/src/utils/ViewSource.tsx new file mode 100644 index 00000000..d82ede8c --- /dev/null +++ b/packages/inula-dev-tools/src/utils/ViewSource.tsx @@ -0,0 +1,21 @@ +/* + * 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 {createContext} from "openinula"; + +const ViewSourceContext = createContext(null); +ViewSourceContext.displayName = 'ViewSourceContext'; + +export default ViewSourceContext; diff --git a/packages/inula-dev-tools/src/utils/constants.ts b/packages/inula-dev-tools/src/utils/constants.ts new file mode 100644 index 00000000..99f36fb8 --- /dev/null +++ b/packages/inula-dev-tools/src/utils/constants.ts @@ -0,0 +1,66 @@ +/* + * 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. + */ + +// panel 页面打开后初始化连接标志 +export const InitDevToolPageConnection = 'init dev tool page connection'; +// background 解析全部 root VNodes 标志 +export const RequestAllVNodeTreeInfos = 'request all vNodes tree infos'; +// vNodes 全部树解析结果标志 +export const AllVNodeTreeInfos = 'vNode trees infos'; +// 一棵树的解析 +export const OneVNodeTreeInfos = 'one vNode tree'; +// 获取组件属性 +export const RequestComponentAttrs = 'get component attrs'; +// 返回组件属性 +export const ComponentAttrs = 'component attrs'; + +export const ModifyAttrs = 'modify attrs'; + +export const ModifyProps = 'modify props'; + +export const ModifyState = 'modify state'; + +export const ModifyHooks = 'modify hooks'; + +export const InspectDom = 'inspect component dom'; + +export const LogComponentData = 'log component data'; + +export const CopyComponentAttr = 'copy component attr'; +// 传递消息来源标志 +export const DevToolPanel = 'dev tool panel'; + +export const DevToolBackground = 'dev tool background'; + +export const DevToolContentScript = 'dev tool content script'; + +export const DevToolHook = 'dev tool hook'; + +export const GetStores = 'get stores'; + +// 高亮显示与消除 +export const Highlight = 'highlight'; +export const RemoveHighlight = 'remove highlight'; + +// 跳转元素代码位置 +export const ViewSource = 'view source'; + +// 选择页面元素 +export const PickElement = 'pick element'; +export const StopPickElement = 'stop pick element'; + +// 复制和存为全局变量 +export const CopyToConsole = 'copy to console'; +export const StorageValue = 'storage value'; diff --git a/packages/inula-dev-tools/src/utils/injectUtils.ts b/packages/inula-dev-tools/src/utils/injectUtils.ts new file mode 100644 index 00000000..7b25df96 --- /dev/null +++ b/packages/inula-dev-tools/src/utils/injectUtils.ts @@ -0,0 +1,47 @@ +/* + * 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. + */ + +function ifNullThrows(value) { + if (value === null) { + throw new Error('receive a null'); + } + + return value; +} + +export function injectSrc(src) { + const script = document.createElement('script'); + script.src = src; + script.type = 'text/javascript'; + script.async = false; + script.onload = function () { + // 加载完毕后需要移除 + script.remove(); + }; + + ifNullThrows( + document.head + || document.getElementsByName('head')[0] + || document.documentElement + ).appendChild(script); +} + +function injectCode(code) { + const script = document.createElement('script'); + script.textContent = code; + + ifNullThrows(document.documentElement).appendChild(script); + ifNullThrows(script.parentNode).removeChild(script); +} diff --git a/packages/inula-dev-tools/src/utils/logUtil.ts b/packages/inula-dev-tools/src/utils/logUtil.ts new file mode 100644 index 00000000..00c1370c --- /dev/null +++ b/packages/inula-dev-tools/src/utils/logUtil.ts @@ -0,0 +1,35 @@ +/* + * 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. + */ + +/** + * chrome 通过 iframe 的方式将 panel 页面嵌入到开发者工具中,如果报错无法感知 + * 同时也无法在运行时打断点,需要适当的日志辅助开发和定位问题 + */ +interface LoggerType { + error: typeof console.error, + info: typeof console.info, + log: typeof console.log, + warn: typeof console.warn +} + +export function createLogger(id: string): LoggerType { + return ['error', 'info', 'log', 'warn'].reduce((pre, current) => { + const prefix = `[inula_dev_tools][${id}] `; + pre[current] = (...data) => { + console[current](prefix, ...data); + }; + return pre; + }, {} as LoggerType); +} diff --git a/packages/inula-dev-tools/src/utils/publicUtil.ts b/packages/inula-dev-tools/src/utils/publicUtil.ts new file mode 100644 index 00000000..9608c766 --- /dev/null +++ b/packages/inula-dev-tools/src/utils/publicUtil.ts @@ -0,0 +1,20 @@ +/* + * 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 { debounce } from 'lodash'; + +export const debounceFunc = debounce(callback => { + callback(); +}, 100); diff --git a/packages/inula-dev-tools/src/utils/regExpUtil.ts b/packages/inula-dev-tools/src/utils/regExpUtil.ts new file mode 100644 index 00000000..07a54a8d --- /dev/null +++ b/packages/inula-dev-tools/src/utils/regExpUtil.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +export function createRegExp(expression: string) { + let str = expression; + if (str[0] === '/') { + str = str.slice(1); + } + if (str[str.length - 1] === '/') { + str = str.slice(0, str.length - 1); + } + try { + return new RegExp(str, 'i'); + } catch (err) { + return null; + } +} diff --git a/packages/inula-dev-tools/src/utils/transferUtils.ts b/packages/inula-dev-tools/src/utils/transferUtils.ts new file mode 100644 index 00000000..ddf03739 --- /dev/null +++ b/packages/inula-dev-tools/src/utils/transferUtils.ts @@ -0,0 +1,47 @@ +/* + * 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 devTools = 'INULA_DEV_TOOLS'; + +interface PayloadType { + type: string; + data?: any; + inulaX?: boolean; +} + +interface Message { + type: typeof devTools; + payload: PayloadType; + from: string; +} + +export function packagePayload(payload: PayloadType, from: string, inulaX?: boolean): Message { + if (inulaX) { + payload.inulaX = true; + } + return { + type: devTools, + payload, + from + }; +} + +export function checkMessage(data: any, from: string) { + return data?.type === devTools && data?.from === from; +} + +export function changeSource(message: Message, from: string) { + message.from = from; +} From bdd04c62260075b83f72473068711eed3dcc8666 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Mon, 16 Oct 2023 11:05:11 +0800 Subject: [PATCH 008/108] =?UTF-8?q?[inula-dev-tools]=20=E4=B8=8E=20c?= =?UTF-8?q?hrome=20=E5=BB=BA=E7=AB=8B=E6=B6=88=E6=81=AF=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-dev-tools/src/background/index.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/packages/inula-dev-tools/src/background/index.ts b/packages/inula-dev-tools/src/background/index.ts index e69de29b..0efe312b 100644 --- a/packages/inula-dev-tools/src/background/index.ts +++ b/packages/inula-dev-tools/src/background/index.ts @@ -0,0 +1,80 @@ +/* + * 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 { checkMessage, packagePayload, changeSource } from "../utils/transferUtils"; +import { RequestAllVNodeTreeInfos, InitDevToolPageConnection, DevToolBackground } from "../utils/constants"; +import { DevToolPanel, DevToolContentScript } from "../utils/constants"; + +// 多个页面 tab 页共享一个 background,需要建立连接池,给每个 tab 建立连接 +export const connections = {}; + +// panel 代码中调用 let backgroundPageConnection = chrome.runtime.connect({...}) 会触发回调函数 +chrome.runtime.onConnect.addListener(function (port) { + function extensionListener(message) { + const isInulaMessage = checkMessage(message, DevToolPanel); + if (isInulaMessage) { + const { payload } = message; + // tabId 值指当前浏览器分配给 web_page 的 id 值。是 panel 页面查询得到,指定向页面发送消息 + const { type, data, tabId } = payload; + let passMessage; + if (type === InitDevToolPageConnection) { + // 记录 panel 所在 tab 页的 tabId,如果已经记录了,覆盖原有 port,因为原有 port 可能关闭了 + // 可能这次是 panel 发起的重新建立请求 + connections[tabId] = port; + passMessage = packagePayload({ type: RequestAllVNodeTreeInfos }, DevToolBackground); + } else { + passMessage = packagePayload({ type, data }, DevToolBackground); + } + chrome.tabs.sendMessage(tabId, passMessage); + } + } + + // 监听 dev tools 页面发送的消息 + port.onMessage.addListener(extensionListener); + + port.onDisconnect.addListener(function (port) { + port.onMessage.removeListener(extensionListener); + + const tabs = Object.keys(connections); + for (let i = 0, len = tabs.length; i < len; i++) { + if (connections[tabs[i]] == port) { + delete connections[tabs[i]]; + break; + } + } + }); +}); + +// 监听来自 content script 的消息,并将消息发送给对应的 dev tools 页面 +chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) { + // 来自 content script 的消息需要预先设置 sender.tab + if (sender.tab) { + const tabId = sender.tab.id; + if (message.payload.type.startsWith('inulax')) { + return; + } + if (tabId && tabId in connections && checkMessage(message, DevToolContentScript)) { + changeSource(message, DevToolBackground); + connections[tabId].postMessage(message); + } else { + // TODO: 如果查询失败,发送 chrome message ,请求 panel 主动建立连接 + console.log('Tab is not found in connection'); + } + } else { + console.log('sender.tab is not defined'); + } + // 需要返回消息告知完成通知,否则会出现报错 message port closed before a response was received + sendResponse({ status: 'ok' }); +}); From 490d4d0946f73706b56ac0dfe66d854817f428cd Mon Sep 17 00:00:00 2001 From: HoikanChen Date: Wed, 18 Oct 2023 10:48:05 +0800 Subject: [PATCH 009/108] =?UTF-8?q?=E4=BF=AE=E6=94=B9codecheck=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=9A//=20@ts-ignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/inula/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx b/packages/inula/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx index 109ccbc2..38b7445b 100644 --- a/packages/inula/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx +++ b/packages/inula/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx @@ -13,7 +13,7 @@ * See the Mulan PSL v2 for more details. */ -//@ts-ignore +// @ts-ignore import * as Inula from '../../../../src/index'; import { createStore, From 7dd1128f03b9bd83f1989e6535e262aa2b11c65e Mon Sep 17 00:00:00 2001 From: HoikanChen Date: Wed, 18 Oct 2023 11:18:44 +0800 Subject: [PATCH 010/108] =?UTF-8?q?=E4=BF=AE=E6=94=B9PR=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitee/PULL_REQUEST_TEMPLATE_CN.md | 11 +---------- .gitee/PULL_REQUEST_TEMPLATE_EN.md | 9 --------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/.gitee/PULL_REQUEST_TEMPLATE_CN.md b/.gitee/PULL_REQUEST_TEMPLATE_CN.md index 8e1f3393..e0708035 100644 --- a/.gitee/PULL_REQUEST_TEMPLATE_CN.md +++ b/.gitee/PULL_REQUEST_TEMPLATE_CN.md @@ -1,17 +1,8 @@ ---- -name: 模板名称,在新建pr时候能看到 -about: 模板描述,对应的pr模板卡片展示时候能看到,介绍模板 ---- **PR 描述:** [请描述提交此 PR 的背景、目的、所做的更改以及如何测试此 PR] **关联的 Issues:** [请列出与此 PR 相关的 issue 编号] -**TODO(可选)** -- [ ] 任务1 -- [ ] ... - -**检查项:** - +**检查项(无需修改,提交后界面上可勾选):** - [ ] 代码已经被检视 - [ ] 代码符合项目的代码标准和最佳实践 - [ ] 代码已经通过所有测试用例 diff --git a/.gitee/PULL_REQUEST_TEMPLATE_EN.md b/.gitee/PULL_REQUEST_TEMPLATE_EN.md index 7f8acc9b..7f2c7abf 100644 --- a/.gitee/PULL_REQUEST_TEMPLATE_EN.md +++ b/.gitee/PULL_REQUEST_TEMPLATE_EN.md @@ -1,17 +1,8 @@ ---- -name: The template name can be seen when creating a new PR -about: Template description, which can be seen when displaying the corresponding PR template card ---- **Description:** [Please describe the background, purpose, changes made, and how to test this PR] **Related Issues:** [List the issue numbers related to this PR] -**TODO(optional)** -- [ ] task1 -- [ ] ... - **Checklist:** - - [ ] Code has been reviewed - [ ] Code complies with the project's code standards and best practices - [ ] Code has passed all tests From d683f90fea095134f88c5044080b8852469333f1 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Wed, 18 Oct 2023 15:18:10 +0800 Subject: [PATCH 011/108] =?UTF-8?q?[inula-dev-tools]=20=E4=B8=8E=20c?= =?UTF-8?q?hrome=20=E5=BB=BA=E7=AB=8B=E6=B6=88=E6=81=AF=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/background/inulaXHandler.ts | 293 ++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 packages/inula-dev-tools/src/background/inulaXHandler.ts diff --git a/packages/inula-dev-tools/src/background/inulaXHandler.ts b/packages/inula-dev-tools/src/background/inulaXHandler.ts new file mode 100644 index 00000000..3899e486 --- /dev/null +++ b/packages/inula-dev-tools/src/background/inulaXHandler.ts @@ -0,0 +1,293 @@ +/* + * 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 { DevToolBackground, DevToolContentScript, DevToolPanel } from '../utils/constants'; +import { connections } from './index'; +import { packagePayload } from '../utils/transferUtils'; + +// 监听来自 content script 的消息,并将消息发送给对应的 dev tools page +const eventsPerTab = {}; +const storesPerTab = {}; +const observedComponents = {}; +const eventPersistencePerTab = {}; +let idGenerator = 1; + +// 当 tab 重新加载,需要对该 tab 所监听的 stores 进行重置 +chrome.tabs.onUpdated.addListener(function (tabId, changeInfo) { + if (changeInfo.status === 'loading') { + if (!eventPersistencePerTab[tabId]) { + eventsPerTab[tabId] = []; + } + storesPerTab[tabId] = []; + } +}); + +function sendTo(connectionId, message) { + if (connections[connectionId]) { + connections[connectionId].postMessage(message); + } +} + +function requestObservedComponents(tabId) { + setTimeout(() => { + chrome.tabs.sendMessage( + tabId, + packagePayload( + { + type: 'inulax request observed components', + data: {} + }, + 'dev tool background' + ) + ); + }, 1); +} + +function executeAction(tabId, storeId, action, params) { + chrome.tabs.sendMessage( + tabId, + packagePayload( + { + type: 'inulax execute action', + data: { + action, + storeId, + params, + } + }, + 'dev tool background' + ) + ); +} + +function queueAction(tabId, storeId, action, params) { + chrome.tabs.sendMessage( + tabId, + packagePayload( + { + type: 'inulax queue action', + data: { + action, + storeId, + params, + } + }, + 'sev tool background' + ) + ); +} + +function getObservedComponents(storeId, tabId) { + if (!observedComponents[tabId]) { + observedComponents[tabId] = {}; + } + if (!observedComponents[tabId][storeId]) { + return []; + } + return observedComponents[tabId][storeId]; +} + +// 来自 content script 的消息 +chrome.runtime.onMessage.addListener(function (message, sender) { + if (message.payload.type.startsWith('inulax')) { + console.log('inulaXHandler message from content script', { + payload: { ...message.payload }, + }); + + if (message.from === DevToolContentScript && sender.tab?.id) { + if (message.payload.type === 'inulax observed components') { + observedComponents[sender.tab.id] = message.payload.data; + + sendTo(sender.tab.id, { + type: 'INULA_DEV_TOOLS', + payload: { + type: 'inulax observed components', + data: message.payload.data, + }, + from: DevToolBackground, + }); + return; + } + requestObservedComponents(sender.tab.id); + + // content script -> inulaXHandler + if (!eventsPerTab[sender.tab.id]) { + eventsPerTab[sender.tab.id] = []; + } + eventsPerTab[sender.tab.id].push({ + id: idGenerator++, + timestamp: Date.now(), + message: message.payload, + }); + + sendTo(sender.tab.id, { + type: 'INULA_DEV_TOOLS', + payload: { + type: 'inulax events', + events: eventsPerTab[sender.tab.id], + }, + from: DevToolBackground, + }); + + // 如果当前 tab 没有 store data,则初始化 + if (!storesPerTab[sender.tab.id]) { + storesPerTab[sender.tab.id] = []; + } + + let found = false; + storesPerTab[sender.tab.id]?.some((store, index) => { + if (store.id === message.payload.data.store.id) { + found = true; + storesPerTab[sender.tab!.id!][index] = message.payload.data.store; + requestObservedComponents(sender.tab?.id); + return true; + } + return false; + }); + + if (!found) { + const tabId = sender.tab.id; + if (!storesPerTab[tabId]) { + storesPerTab[tabId] = []; + } + storesPerTab[tabId].push(message.payload.data.store); + sendTo(tabId, { + type: 'INULA_DEV_TOOLS', + payload: { + type: 'inulax stores', + stores: storesPerTab[tabId]?.map(store => { + // 连接被监测的组件 + requestObservedComponents(tabId); + const observedComponents = getObservedComponents(store, tabId); + return { ...store, observedComponents }; + }) || [], + newStore: message.payload.data.store.id, + }, + from: DevToolBackground, + }); + return; + } + + sendTo(sender.tab.id, { + type: 'INULA_DEV_TOOLS', + payload: { + type: 'inulax stores', + stores: storesPerTab[sender.tab.id]?.map(store => { + // 连接被监测的组件 + const observedComponents = getObservedComponents(store, sender.tab?.id); + return { ...store, observedComponents }; + }) || [], + updated: message.payload.data.store.id, + }, + from: DevToolBackground, + }); + + requestObservedComponents(message.payload.tabId); + } + + if (message.from === DevToolPanel) { + // panel -> inulaXHandler + if (message.payload.type === 'inulax run action') { + executeAction( + message.payload.tabId, + message.payload.storeId, + message.payload.action, + message.payload.args + ); + return; + } + + if (message.payload.type === 'inulax change state') { + chrome.tabs.sendMessage( + message.payload.tabId, + packagePayload(message.payload, 'dev tool background') + ); + return; + } + + if (message.payload.type === 'inulax queue action') { + queueAction( + message.payload.tabId, + message.payload.storeId, + message.payload.action, + message.payload.args + ); + return; + } + + if (message.payload.type === 'inulax resetEvents') { + eventsPerTab[message.payload.tabId] = []; + sendTo(message.payload.tabId, { + type: 'INULA_DEV_TOOLS', + payload: { + type: 'inulax events', + events: eventsPerTab[message.payload.tabId], + }, + from: DevToolBackground, + }); + return; + } + + if (message.payload.type === 'inula setPersistent'){ + const { tabId, persistent } = message.payload; + eventPersistencePerTab[tabId] = persistent; + return; + } + + if (message.payload.type === 'inulax getPersistence') { + sendTo(message.payload.tabId, { + type: 'INULA_DEV_TOOLS', + payload: { + type: 'inulax persistence', + persistent: !!eventPersistencePerTab[message.payload.tabId], + }, + from: DevToolBackground, + }); + return; + } + + if (message.payload.type === 'inulax getEvents') { + if (!eventsPerTab[message.payload.tabId]) { + eventsPerTab[message.payload.tabId] = []; + } + sendTo(message.payload.tabId, { + type: 'INULA_DEV_TOOLS', + payload: { + type: 'inulax events', + events: eventsPerTab[message.payload.tabId], + }, + from: DevToolBackground, + }); + return; + } + + if (message.payload.type === 'inulax getStores') { + sendTo(message.payload.tabId, { + type: 'INULA_DEV_TOOLS', + payload: { + type: 'inulax stores', + stores: storesPerTab[message.payload.tabId]?.map(store => { + requestObservedComponents(message.payload.tabId); + const observedComponents = getObservedComponents(store.id, message.payload.tabId); + return { ...store, observedComponents }; + }) || [], + }, + from: DevToolBackground, + }); + return; + } + } + } +}); From 8e3385cc03014fcdc05b3746530d30274277addb Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Wed, 18 Oct 2023 15:19:12 +0800 Subject: [PATCH 012/108] =?UTF-8?q?[inula-dev-tools]=20=E4=B8=8E=20c?= =?UTF-8?q?hrome=20=E5=BB=BA=E7=AB=8B=E6=B6=88=E6=81=AF=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-dev-tools/src/background/inulaXHandler.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/inula-dev-tools/src/background/inulaXHandler.ts b/packages/inula-dev-tools/src/background/inulaXHandler.ts index 3899e486..4c7e9c2b 100644 --- a/packages/inula-dev-tools/src/background/inulaXHandler.ts +++ b/packages/inula-dev-tools/src/background/inulaXHandler.ts @@ -280,7 +280,10 @@ chrome.runtime.onMessage.addListener(function (message, sender) { type: 'inulax stores', stores: storesPerTab[message.payload.tabId]?.map(store => { requestObservedComponents(message.payload.tabId); - const observedComponents = getObservedComponents(store.id, message.payload.tabId); + const observedComponents = getObservedComponents( + store.id, + message.payload.tabId + ); return { ...store, observedComponents }; }) || [], }, From 6e326761e8d16a3ea0d3e7d69b0c5633262049f5 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Thu, 19 Oct 2023 19:34:44 +0800 Subject: [PATCH 013/108] =?UTF-8?q?[inula-dev-tools]=20=E8=B0=83?= =?UTF-8?q?=E8=AF=95=E5=B7=A5=E5=85=B7=20svgs=20=E5=90=88=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-dev-tools/src/svgs/Arrow.tsx | 33 ++++++++++++++++ packages/inula-dev-tools/src/svgs/Close.tsx | 22 +++++++++++ packages/inula-dev-tools/src/svgs/Debug.tsx | 27 +++++++++++++ .../inula-dev-tools/src/svgs/Discover.tsx | 26 +++++++++++++ packages/inula-dev-tools/src/svgs/Eye.tsx | 26 +++++++++++++ .../inula-dev-tools/src/svgs/Location.tsx | 24 ++++++++++++ .../inula-dev-tools/src/svgs/Operation.tsx | 22 +++++++++++ packages/inula-dev-tools/src/svgs/Select.tsx | 39 +++++++++++++++++++ .../inula-dev-tools/src/svgs/Triangle.tsx | 32 +++++++++++++++ packages/inula-dev-tools/src/svgs/copy.svg | 20 ++++++++++ packages/inula-dev-tools/src/svgs/storage.svg | 19 +++++++++ 11 files changed, 290 insertions(+) create mode 100644 packages/inula-dev-tools/src/svgs/Arrow.tsx create mode 100644 packages/inula-dev-tools/src/svgs/Close.tsx create mode 100644 packages/inula-dev-tools/src/svgs/Debug.tsx create mode 100644 packages/inula-dev-tools/src/svgs/Discover.tsx create mode 100644 packages/inula-dev-tools/src/svgs/Eye.tsx create mode 100644 packages/inula-dev-tools/src/svgs/Location.tsx create mode 100644 packages/inula-dev-tools/src/svgs/Operation.tsx create mode 100644 packages/inula-dev-tools/src/svgs/Select.tsx create mode 100644 packages/inula-dev-tools/src/svgs/Triangle.tsx create mode 100644 packages/inula-dev-tools/src/svgs/copy.svg create mode 100644 packages/inula-dev-tools/src/svgs/storage.svg diff --git a/packages/inula-dev-tools/src/svgs/Arrow.tsx b/packages/inula-dev-tools/src/svgs/Arrow.tsx new file mode 100644 index 00000000..0e6dd813 --- /dev/null +++ b/packages/inula-dev-tools/src/svgs/Arrow.tsx @@ -0,0 +1,33 @@ +/* + * 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. + */ + +interface IArrow { + direction: 'up' | 'down' +} + +export default function Arrow({ direction: director }: IArrow) { + let d = ''; + if (director === 'up') { + d = 'M4 9.5 L5 10.5 L8 7.5 L11 10.5 L12 9.5 L8 5.5 z'; + } else if (director === 'down') { + d = 'M5 5.5 L4 6.5 L8 10.5 L12 6.5 L11 5.5 L8 8.5z'; + } + + return ( + + + + ); +} diff --git a/packages/inula-dev-tools/src/svgs/Close.tsx b/packages/inula-dev-tools/src/svgs/Close.tsx new file mode 100644 index 00000000..4d8cfd81 --- /dev/null +++ b/packages/inula-dev-tools/src/svgs/Close.tsx @@ -0,0 +1,22 @@ +/* + * 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. + */ + +export default function Close() { + return ( + + + + ); +} diff --git a/packages/inula-dev-tools/src/svgs/Debug.tsx b/packages/inula-dev-tools/src/svgs/Debug.tsx new file mode 100644 index 00000000..f50f8ddb --- /dev/null +++ b/packages/inula-dev-tools/src/svgs/Debug.tsx @@ -0,0 +1,27 @@ +/* + * 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. + */ + +export default function Debug() { + return ( + + + + + ); +} diff --git a/packages/inula-dev-tools/src/svgs/Discover.tsx b/packages/inula-dev-tools/src/svgs/Discover.tsx new file mode 100644 index 00000000..defd1702 --- /dev/null +++ b/packages/inula-dev-tools/src/svgs/Discover.tsx @@ -0,0 +1,26 @@ +/* + * 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. + */ + +export default function Discover() { + return ( + + + + + ); +} diff --git a/packages/inula-dev-tools/src/svgs/Eye.tsx b/packages/inula-dev-tools/src/svgs/Eye.tsx new file mode 100644 index 00000000..c3fc2582 --- /dev/null +++ b/packages/inula-dev-tools/src/svgs/Eye.tsx @@ -0,0 +1,26 @@ +/* + * 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. + */ + +export default function Eye() { + return ( + + + + + ); +} diff --git a/packages/inula-dev-tools/src/svgs/Location.tsx b/packages/inula-dev-tools/src/svgs/Location.tsx new file mode 100644 index 00000000..40e6c713 --- /dev/null +++ b/packages/inula-dev-tools/src/svgs/Location.tsx @@ -0,0 +1,24 @@ +/* + * 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. + */ + +export default function Location() { + return ( + + + + + ); +} diff --git a/packages/inula-dev-tools/src/svgs/Operation.tsx b/packages/inula-dev-tools/src/svgs/Operation.tsx new file mode 100644 index 00000000..13831192 --- /dev/null +++ b/packages/inula-dev-tools/src/svgs/Operation.tsx @@ -0,0 +1,22 @@ +/* + * 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. + */ + +export default function Operation() { + return ( + + + + ); +} diff --git a/packages/inula-dev-tools/src/svgs/Select.tsx b/packages/inula-dev-tools/src/svgs/Select.tsx new file mode 100644 index 00000000..86c6c1ea --- /dev/null +++ b/packages/inula-dev-tools/src/svgs/Select.tsx @@ -0,0 +1,39 @@ +/* + * 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. + */ + +export default function Select() { + return ( + + + + + + + + + + ); +} diff --git a/packages/inula-dev-tools/src/svgs/Triangle.tsx b/packages/inula-dev-tools/src/svgs/Triangle.tsx new file mode 100644 index 00000000..49ed3afd --- /dev/null +++ b/packages/inula-dev-tools/src/svgs/Triangle.tsx @@ -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. + */ + +interface IArrow { + director: 'right' | 'down' +} + +export default function Triangle({ director }: IArrow) { + let d = ''; + if (director === 'right') { + d = 'm2 0l12 8l-12 8 z'; + } else if (director === 'down') { + d = 'm0 2h16 l-8 12 z'; + } + return ( + + + + ); +} diff --git a/packages/inula-dev-tools/src/svgs/copy.svg b/packages/inula-dev-tools/src/svgs/copy.svg new file mode 100644 index 00000000..48431e0e --- /dev/null +++ b/packages/inula-dev-tools/src/svgs/copy.svg @@ -0,0 +1,20 @@ + + + + + + + diff --git a/packages/inula-dev-tools/src/svgs/storage.svg b/packages/inula-dev-tools/src/svgs/storage.svg new file mode 100644 index 00000000..470821fc --- /dev/null +++ b/packages/inula-dev-tools/src/svgs/storage.svg @@ -0,0 +1,19 @@ + + + + + + From 0f14d48a77df47e5e633498d72690963ff803f75 Mon Sep 17 00:00:00 2001 From: LogCreative Date: Fri, 20 Oct 2023 15:40:11 +0800 Subject: [PATCH 014/108] =?UTF-8?q?[inula-intl]=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E7=A9=BA=E6=A0=BC=E7=BC=A9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-intl/example/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/inula-intl/example/App.tsx b/packages/inula-intl/example/App.tsx index be1cf84c..205a38f1 100644 --- a/packages/inula-intl/example/App.tsx +++ b/packages/inula-intl/example/App.tsx @@ -32,7 +32,7 @@ const App = () => { const message = locale === 'zh' ? zh : en - return ( + return (
Inula-Intl API Test Demo
From 012c3107be834fce5c187655e8335387bfe1ba49 Mon Sep 17 00:00:00 2001 From: ZHITENGLI Date: Fri, 20 Oct 2023 15:59:01 +0800 Subject: [PATCH 015/108] Use the opposite operator ("!==") instead. --- packages/inula/src/renderer/utils/compare.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/inula/src/renderer/utils/compare.ts b/packages/inula/src/renderer/utils/compare.ts index 95e0f3a5..de5d58ca 100644 --- a/packages/inula/src/renderer/utils/compare.ts +++ b/packages/inula/src/renderer/utils/compare.ts @@ -17,7 +17,7 @@ * 兼容IE浏览器没有Object.is */ export function isSame(x: any, y: any) { - if (!(typeof Object.is === 'function')) { + if (typeof Object.is !== 'function') { if (x === y) { // +0 != -0 return x !== 0 || 1 / x === 1 / y; From 7b1b08c5529e9acf599e8a7eb690970742f1f1e6 Mon Sep 17 00:00:00 2001 From: zhangchenqi123 <1149440709@qq.com> Date: Fri, 20 Oct 2023 16:56:07 +0800 Subject: [PATCH 016/108] Change the double quotation marks of a string to single quotation marks --- .../inula-dev-tools/src/utils/Checkbox.tsx | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/inula-dev-tools/src/utils/Checkbox.tsx b/packages/inula-dev-tools/src/utils/Checkbox.tsx index f29f6e8b..e28782fa 100644 --- a/packages/inula-dev-tools/src/utils/Checkbox.tsx +++ b/packages/inula-dev-tools/src/utils/Checkbox.tsx @@ -17,25 +17,25 @@ export function Checkbox({ value }) { return (
From fac236f0fe149a8a523e5b3f0bb060733d52c416 Mon Sep 17 00:00:00 2001 From: AnkorTn <1114655801@qq.com> Date: Fri, 20 Oct 2023 15:34:40 +0800 Subject: [PATCH 017/108] fix: Unexpected use of continue statement. --- packages/create-inula/lib/BasicGenerator.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/create-inula/lib/BasicGenerator.js b/packages/create-inula/lib/BasicGenerator.js index 0486b42c..4b5c66f9 100644 --- a/packages/create-inula/lib/BasicGenerator.js +++ b/packages/create-inula/lib/BasicGenerator.js @@ -140,9 +140,10 @@ class BasicGenerator extends Generator { if (fs.lstatSync(fullpath).isDirectory()) { this.traverseDirBubble(fullpath, dirCallback, fileCallback); dirCallback(fullpath); - continue; } - fileCallback(fullpath); + else{ + fileCallback(fullpath); + } } } From bb5025bef9e2e5bf627b3571d6433bf325de8b70 Mon Sep 17 00:00:00 2001 From: AliciaRussel Date: Thu, 26 Oct 2023 17:35:42 +0800 Subject: [PATCH 018/108] =?UTF-8?q?=E6=8A=8A=E5=AD=97=E7=AC=A6=E4=B8=B2?= =?UTF-8?q?=E7=9A=84=E5=8F=8C=E5=BC=95=E5=8F=B7=E6=94=B9=E6=88=90=E5=8D=95?= =?UTF-8?q?=E5=BC=95=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-dev-tools/src/utils/ViewSource.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/inula-dev-tools/src/utils/ViewSource.tsx b/packages/inula-dev-tools/src/utils/ViewSource.tsx index d82ede8c..0b07584f 100644 --- a/packages/inula-dev-tools/src/utils/ViewSource.tsx +++ b/packages/inula-dev-tools/src/utils/ViewSource.tsx @@ -13,7 +13,7 @@ * See the Mulan PSL v2 for more details. */ -import {createContext} from "openinula"; +import {createContext} from 'openinula'; const ViewSourceContext = createContext(null); ViewSourceContext.displayName = 'ViewSourceContext'; From c0d376e62f3cf541fd75fecfb5ad8dd314fa4aa4 Mon Sep 17 00:00:00 2001 From: ch1y0q Date: Fri, 27 Oct 2023 07:33:04 +0000 Subject: [PATCH 019/108] specify code language for packages/inula-cli/README.md. Signed-off-by: ch1y0q --- packages/inula-cli/README.md | 75 +++++++++++++++++------------------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/packages/inula-cli/README.md b/packages/inula-cli/README.md index 5e26db29..bebd176e 100644 --- a/packages/inula-cli/README.md +++ b/packages/inula-cli/README.md @@ -2,23 +2,23 @@ ## 一、安装使用 -### 安装Nodejs +### 安装Node.js -inula-cli的运行需要依赖Nodejs,使用前请确保您的电脑已安装Nodejs,并且版本在16以上。您可以通过在控制台执行以下命令来确认您的版本。 +inula-cli的运行需要依赖Node.js,使用前请确保您的电脑已安装Node.js,并且版本在16以上。您可以通过在控制台执行以下命令来确认您的版本。 -``` +```shell >node -v v16.4.0 ``` -如果您没有安装Nodejs,或者Nodejs版本不满足条件,推荐使用nvm工具安装和管理Nodejs版本。 +如果您没有安装Node.js,或者Node.js版本不满足条件,推荐使用nvm工具安装和管理Node.js版本。 nvm最新版本下载: [https://github.com/coreybutler/nvm-windows/releases](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fcoreybutler%2Fnvm-windows%2Freleases) -安装nvm之后,可以通过如下命令安装Nodejs: +安装nvm之后,可以通过如下命令安装Node.js: -``` +```shell >node install 16 >node use 16 @@ -28,15 +28,15 @@ nvm最新版本下载: [https://github.com/coreybutler/nvm-windows/releases](htt ### 安装inula-cli -为了方便使用inula-cli的功能,推荐您全局安装inula-cli。Nodejs安装会自带npm工具用于管理模块,您可以直接运行如下命令: +为了方便使用inula-cli的功能,推荐您全局安装inula-cli。Node.js安装会自带npm工具用于管理模块,您可以直接运行如下命令: -``` +```shell >npm install -g inula-cli ``` 安装完成后,使用inula-cli version命令确认安装是否完成。 -``` +```shell >inula-cli version 1.1.0 ``` @@ -93,7 +93,7 @@ inula-cli支持用户通过项目根目录下的.inula.ts或者.inula.js文件 在配置文件中,您需要默认导出一个配置,以下为一个简单的配置文件示例: -``` +```typescript // .inula.ts export default { @@ -113,7 +113,7 @@ export default { 对于TypeScript类型,我们也提供了类型定义以供开发时自动补全: -``` +```typescript // .inula.ts import { defineConfig } from "inula-cli" @@ -164,8 +164,8 @@ inula-cli的所有功能都围绕插件展开,插件可以很方便地让用 内置插件在inula-cli运行时会自动加载,用户可以直接调用这些内置命令,当前支持的内置插件功能如下: -| 序号 | 插件功能 | 触发命令 | -| ---- | :----------------------- | ------------------- | +| 序号 | 插件功能 | 触发命令 | +| ---- | :----------------------- | ----------------- | | 1、 | 本地开发构建 | inula-cli dev | | 2、 | 生产构建 | inula-cli build | | 3、 | 接口mock能力 | inula-cli dev | @@ -180,13 +180,13 @@ inula-cli支持用户集成已发布在npm仓库的插件,用户可以按需 安装可以通过npm安装,这里以插件@inula/add为例: -``` +```shell npm i --save-dev @inula/add ``` 如果需要运行插件,需要在配置文件中配置对应的插件路径 -``` +```typescript // .inula.ts export default { @@ -205,7 +205,7 @@ export default { 1、编写命令插件文件,这里我们自定义了一个conf命令用于展示当前项目的配置信息。 -``` +```typescript // conf.ts import { API } from "inula-cli"; @@ -224,7 +224,7 @@ export default (api: API) => { 2、在配置文件中加入对插件的引用 -``` +```typescript // .inula.ts export default { @@ -232,9 +232,9 @@ export default { } ``` -3、在项目根目录下执行inula-cli conf即可触发插件运行。 +3、在项目根目录下执行`inula-cli conf`即可触发插件运行。 -``` +```shell > inula-cli conf current user config is: { plugins: [ './conf', './showConf' ], @@ -247,7 +247,7 @@ inula-cli提供了hook机制可以让开发者在执行命令时实现事件监 1、使用插件注册hook -``` +```typescript // modifyConfig.ts import { API } from "inula-cli"; @@ -267,7 +267,7 @@ export default (api: API) => { 2、在插件中触发hook -``` +```typescript // conf.ts import { API } from "inula-cli"; @@ -287,7 +287,7 @@ export default (api: API) => { 3、在配置文件中加入插件 -``` +```typescript // .inula.ts export default { @@ -297,7 +297,7 @@ export default { 4、触发命令 -``` +```shell > inula-cli conf current user config is: { plugins: [ './conf', './showConf' ], @@ -327,7 +327,7 @@ current user config is: { registerCommand方法允许用户自定义inula-cli的执行命令, -``` +```typescript api.registerCommand({ name: string, description?: string, @@ -343,7 +343,7 @@ registerCommand方法允许用户自定义inula-cli的执行命令, 使用示例: -``` +```typescript import { API } from "inula-cli"; export default (api: API) => { @@ -374,7 +374,7 @@ api.registerHook({ 使用示例: -``` +```typescript import { API } from "inula-cli"; export default (api: API) => { @@ -403,7 +403,7 @@ applyHook(name: string, value?: any}) 使用示例: -``` +```typescript import { API } from "inula-cli"; export default (api: API) => { @@ -427,7 +427,7 @@ export default (api: API) => { inula-cli默认集成生产构建能力,用户可以通过在.inula.ts中配置buildConfig字段启用功能。配置示例如下: -``` +```typescript // .inula.ts // 使用webpack构建 @@ -460,7 +460,7 @@ export default { 生产构建支持传入多个配置文件路径,使用webpack构建还支持配置文件以函数方式导出,inula-cli会将配置中的env和args作为参数传递到函数中执行以获取最后的构建配置。 -``` +```typescript // webpack.config.js module.exports = function (env, argv) { @@ -497,7 +497,7 @@ export default { inula-cli默认也支持项目本地构建,用户可以通过在.inula.ts中配置devBuildConfig字段启用功能。配置示例如下: -``` +```typescript // .inula.ts // 使用webpack构建 @@ -552,7 +552,7 @@ inula-cli自动将项目根路径里/Mock目录下所有文件视为mock文件 如果您想修改Mock目录位置,可以在配置文件中修改mockPath。如果不配置该参数,默认使用"./mock"。 -``` +```typescript // .inula.ts export default { ... @@ -569,7 +569,7 @@ export default { Mock文件需要默认导出一个对象,key为"请求方式 接口名",值为接口实现。示例如下: -``` +```typescript export default { "GET /api/user": (req, res) => { res.status(200).json("admin") @@ -579,7 +579,7 @@ export default { 如果想要一次mock多个接口,可以在导出对象中设置多个key,例如: -``` +```typescript export default { "GET /api/user": (req, res) => { res.status(200).json("admin"); @@ -597,7 +597,7 @@ export default { Mock文件默认导出一个数组,数组每一个成员示例如下: -``` +```typescript export default [ { url: '/api/get', @@ -654,7 +654,7 @@ export default [ 在框架配置文件中,开发者需要配置远端服务器地址以及编写自定义的matcher函数提供给框架: -``` +```typescript // .inula.ts const matcher = (pathname, request) => { @@ -689,7 +689,7 @@ export default { 用户可以在.inula.ts中配置remoteProxy字段开启远端静态接口代理能力,完成配置后,使用后执行inula-cli proxy启动该功能。 -``` +```typescript // .inula.ts export default { @@ -710,6 +710,3 @@ export default { } } ``` - - - From 6e7b92583ec10ee2f92912cf16b794abafe192f1 Mon Sep 17 00:00:00 2001 From: LogCreative Date: Fri, 27 Oct 2023 15:33:07 +0800 Subject: [PATCH 020/108] =?UTF-8?q?[inlua-intl]=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-intl/src/format/Translation.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/inula-intl/src/format/Translation.ts b/packages/inula-intl/src/format/Translation.ts index b1f57f79..84b35baf 100644 --- a/packages/inula-intl/src/format/Translation.ts +++ b/packages/inula-intl/src/format/Translation.ts @@ -13,7 +13,6 @@ * See the Mulan PSL v2 for more details. */ -import { UNICODE_REG } from '../constants'; import { CompiledMessage, Locale, LocaleConfig, Locales } from '../types/types'; import generateFormatters from './generateFormatters'; import {FormatOptions, I18nCache} from '../types/interfaces'; From 16212d2668740415f5e5f7ec0406bf2695ee590f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E4=BF=8A=E6=B6=B5?= <9535692+kilostar@user.noreply.gitee.com> Date: Fri, 27 Oct 2023 07:33:14 +0000 Subject: [PATCH 021/108] =?UTF-8?q?update=20README.md.=20=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E6=8D=A2=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 刘俊涵 <9535692+kilostar@user.noreply.gitee.com> --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e155d9c2..c0bef608 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ inula-cli 是一套针对 openInula 的编译期插件,它支持代码优化 欢迎访问 openInula 官网与文档仓库,参与 openInula 开发者文档开源项目,与我们一起完善开发者文档。 openInula 官网地址:[https://www.openinula.net/](https://www.openinula.net/) + openInula 文档站地址:[https://docs.openinula.net/](https://docs.openinula.net/) ## 代码仓地址 From 307a72efaa4b9194d4c03a116c3aa83779c06a0b Mon Sep 17 00:00:00 2001 From: ZHITENGLI Date: Fri, 27 Oct 2023 15:33:17 +0800 Subject: [PATCH 022/108] Use the opposite operator ("!==") instead. --- packages/inula/src/inulax/CommonUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/inula/src/inulax/CommonUtils.ts b/packages/inula/src/inulax/CommonUtils.ts index a6c47181..5202a200 100644 --- a/packages/inula/src/inulax/CommonUtils.ts +++ b/packages/inula/src/inulax/CommonUtils.ts @@ -68,7 +68,7 @@ export function isPromise(obj: any): boolean { } export function isSame(x, y) { - if (!(typeof Object.is === 'function')) { + if (typeof Object.is !== 'function') { if (x === y) { // +0 != -0 return x !== 0 || 1 / x === 1 / y; From e8d96c8ea7f33183de677a36b943334210e7e6bd Mon Sep 17 00:00:00 2001 From: lisr <747702750@qq.com> Date: Fri, 27 Oct 2023 15:35:39 +0800 Subject: [PATCH 023/108] delete an empty line --- packages/inula-cli/src/types/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/inula-cli/src/types/types.ts b/packages/inula-cli/src/types/types.ts index 222c8e05..23bde194 100644 --- a/packages/inula-cli/src/types/types.ts +++ b/packages/inula-cli/src/types/types.ts @@ -165,4 +165,3 @@ export interface Arguments { '--'?: Array; [argName: string]: any; } - From 478fe0616e23f9095abd291a2615186cbfde2792 Mon Sep 17 00:00:00 2001 From: trick-treat <1076689101@qq.com> Date: Fri, 27 Oct 2023 15:38:08 +0800 Subject: [PATCH 024/108] Improve formatting for README. --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e155d9c2..b233d8b6 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ### 核心能力 -**响应式API** +#### 响应式API * openInula 通过最小化重新渲染的范围,从而进行高效的UI渲染。这种方式避免了虚拟 DOM 的开销,使得 openInula 在性能方面表现出色。 * openInula 通过比较变化前后的 JavaScript 对象以细粒度的依赖追踪机制来实现响应式更新,无需用户过度关注性能优化。 @@ -18,36 +18,36 @@ 1. openInula 提供了两组简洁直观的API--响应式 API 和与 React 一致的传统 API,使得开发者可以轻松地构建复杂的交互式界面。 2. openInula 简洁的 API 极大降低了开发者的学习成本,开发者使用响应式API可以快速构建高效的前端界面。 -**兼容 ReactAPI** +#### 兼容 ReactAPI * 与 React 保持一致 API 的特性、可以无缝支持 React 生态。 * 使用传统 API 可以无缝将 React 项目切换至 openInula,React 应用可零修改切换至 openInula。 ### openInula 配套组件 -**状态管理器/inula-X** +#### 状态管理器 → inula-X inula-X 是 openInula 默认提供的状态管理器,无需额外引入三方库,就可以简单实现跨组件/页面共享状态。 inula-X 与 Redux 比可创建多个 Store,不需要在 Reducer 中返回 state 并且简化了 Action 和 Reducer 的创建步骤,原生支持异步能力,组件能做到精准重渲染。inula-X 均可使用函数组件、class 组件,能提供 redux 的适配接口及支持响应式的特点。 -**路由/inula-router** +#### 路由 → inula-router inula-router 是 openInula 生态组建的一部分,为 openInula 提供前端路由的能力,是构建大型应用必要组件。 inula-router 涵盖 react-router、history、connect-react-router 的功能。 -**请求/inula-request** +#### 请求 → inula-request inula-request 是 openInula 生态组件,涵盖常见的网络请求方式,并提供动态轮询钩子函数给用户更便捷的定制化请求体验。 -**国际化/inula-intl** +#### 国际化 → inula-intl inula-intl 是基于 openInula 生态组件,其主要提供了国际化功能,涵盖了基本的国际化组件和钩子函数,便于用户在构建国际化能力时方便操作。 -**调试工具/inula-dev-tools** +#### 调试工具 → inula-dev-tools inula-dev-tools 是一个为 openInula 开发者提供的强大工具集,能够方便地查看和编辑组件树、管理应用状态以及进行性能分析,极大提高了开发效率和诊断问题的便捷性。 -**脚手架/inula-cli** +#### 脚手架 → inula-cli inula-cli 是一套针对 openInula 的编译期插件,它支持代码优化、JSX 语法转换以及代码分割,有助于提高应用的性能、可读性和可维护性。 @@ -55,8 +55,8 @@ inula-cli 是一套针对 openInula 的编译期插件,它支持代码优化 欢迎访问 openInula 官网与文档仓库,参与 openInula 开发者文档开源项目,与我们一起完善开发者文档。 -openInula 官网地址:[https://www.openinula.net/](https://www.openinula.net/) -openInula 文档站地址:[https://docs.openinula.net/](https://docs.openinula.net/) ++ openInula 官网地址:[https://www.openinula.net/](https://www.openinula.net/) ++ openInula 文档站地址:[https://docs.openinula.net/](https://docs.openinula.net/) ## 代码仓地址 @@ -71,7 +71,7 @@ openInula 仓库地址:[https://gitee.com/openinula](https://gitee.com/openinu ## 许可协议 -openInula 主要遵循 Mulan Permissive Software License v2 协议,详情请参考各代码仓 LICENSE 声明。 +openInula 主要遵循 [Mulan Permissive Software License v2](http://license.coscl.org.cn/MulanPSL2) 协议,详情请参考各代码仓 LICENSE 声明。 ## 联系方式 From bd84a494926974a5240bf5cb4ebff7f9502b2258 Mon Sep 17 00:00:00 2001 From: Catchiz <1375535806@qq.com> Date: Fri, 27 Oct 2023 07:38:12 +0000 Subject: [PATCH 025/108] =?UTF-8?q?=E5=8E=9F=E6=9C=AC=E6=98=AFform?= =?UTF-8?q?=E4=BD=9C=E4=B8=BA=E5=8F=98=E9=87=8F=E5=90=8D=EF=BC=8C=E4=BD=86?= =?UTF-8?q?=E6=98=AF=E6=8C=87=E4=BB=A3=E7=9A=84=E6=98=AFfrom=E7=9A=84key?= =?UTF-8?q?=EF=BC=8C=E8=AF=AD=E4=B9=89=E4=B8=8D=E6=B8=85=E6=99=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Catchiz <1375535806@qq.com> --- packages/inula-router/src/history/utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/inula-router/src/history/utils.ts b/packages/inula-router/src/history/utils.ts index 0fe235a2..cfa49b7a 100644 --- a/packages/inula-router/src/history/utils.ts +++ b/packages/inula-router/src/history/utils.ts @@ -109,12 +109,12 @@ export function stripBasename(path: string, prefix: string): string { export function createMemoryRecord(initVal: S, fn: (arg: S) => T) { let visitedRecord: T[] = [fn(initVal)]; - function getDelta(to: S, form: S): number { - let toIdx = visitedRecord.lastIndexOf(fn(to)); + function getDelta(toKey: S, fromKey: S): number { + let toIdx = visitedRecord.lastIndexOf(fn(toKey)); if (toIdx === -1) { toIdx = 0; } - let fromIdx = visitedRecord.lastIndexOf(fn(form)); + let fromIdx = visitedRecord.lastIndexOf(fn(fromKey)); if (fromIdx === -1) { fromIdx = 0; } From 5cf1d2f45e2967a3900964683786e95d131030d8 Mon Sep 17 00:00:00 2001 From: tzhtaylor <12352549+tzhtaylor@user.noreply.gitee.com> Date: Fri, 27 Oct 2023 07:38:26 +0000 Subject: [PATCH 026/108] Fix: Unexpected use of continue statement. Signed-off-by: tzhtaylor <12352549+tzhtaylor@user.noreply.gitee.com> --- packages/create-inula/lib/BasicGenerator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/create-inula/lib/BasicGenerator.js b/packages/create-inula/lib/BasicGenerator.js index 4b5c66f9..972b2044 100644 --- a/packages/create-inula/lib/BasicGenerator.js +++ b/packages/create-inula/lib/BasicGenerator.js @@ -128,9 +128,9 @@ class BasicGenerator extends Generator { if (fs.existsSync(fullpath)) { this.traverseDirCapture(fullpath, dirCallback, fileCallback); } - continue; + } else { + fileCallback(fullpath); } - fileCallback(fullpath); } } From a24c96cca6b97068ed3cff0247768acb6a073951 Mon Sep 17 00:00:00 2001 From: Zhang_Yaoyun <1764466133@qq.com> Date: Fri, 27 Oct 2023 08:15:01 +0000 Subject: [PATCH 027/108] update README.md. Signed-off-by: Zhang_Yaoyun <1764466133@qq.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c0bef608..f91df5cb 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ **兼容 ReactAPI** -* 与 React 保持一致 API 的特性、可以无缝支持 React 生态。 +* 与React保持一致、可以无缝支持 React 生态。 * 使用传统 API 可以无缝将 React 项目切换至 openInula,React 应用可零修改切换至 openInula。 ### openInula 配套组件 @@ -28,7 +28,7 @@ **状态管理器/inula-X** inula-X 是 openInula 默认提供的状态管理器,无需额外引入三方库,就可以简单实现跨组件/页面共享状态。 -inula-X 与 Redux 比可创建多个 Store,不需要在 Reducer 中返回 state 并且简化了 Action 和 Reducer 的创建步骤,原生支持异步能力,组件能做到精准重渲染。inula-X 均可使用函数组件、class 组件,能提供 redux 的适配接口及支持响应式的特点。 +inula-X 与 Redux 相比,可创建多个 Store,不需要在 Reducer 中返回 state 并且简化了 Action 和 Reducer 的创建步骤,原生支持异步能力,组件能做到精准重渲染。inula-X 均可使用函数组件、class 组件,能提供 redux 的适配接口及支持响应式的特点。 **路由/inula-router** From 31b5b473c61154d746f559e79a7695231c147c60 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 31 Oct 2023 15:54:18 +0800 Subject: [PATCH 028/108] =?UTF-8?q?[inula-request]=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E8=AF=B7=E6=B1=82=E5=8F=96=E6=B6=88=E6=97=B6=20isCanc?= =?UTF-8?q?el=20=E5=88=A4=E6=96=AD=E9=94=99=E8=AF=AF=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cancelRequest/cancelRequestTest.html | 45 ++++++++----------- .../inula-request/src/cancel/CancelError.ts | 2 +- .../inula-request/src/cancel/checkCancel.ts | 2 +- packages/inula-request/src/core/IrError.ts | 1 + .../inula-request/src/request/fetchRequest.ts | 17 +++---- .../src/request/processRequest.ts | 14 ++++-- 6 files changed, 40 insertions(+), 41 deletions(-) diff --git a/packages/inula-request/examples/cancelRequest/cancelRequestTest.html b/packages/inula-request/examples/cancelRequest/cancelRequestTest.html index 9b17da87..f9a4ebcd 100644 --- a/packages/inula-request/examples/cancelRequest/cancelRequestTest.html +++ b/packages/inula-request/examples/cancelRequest/cancelRequestTest.html @@ -16,42 +16,33 @@ diff --git a/packages/inula-request/src/cancel/CancelError.ts b/packages/inula-request/src/cancel/CancelError.ts index fbc0d32d..85717094 100644 --- a/packages/inula-request/src/cancel/CancelError.ts +++ b/packages/inula-request/src/cancel/CancelError.ts @@ -17,7 +17,7 @@ import IrError from '../core/IrError'; import { IrRequestConfig } from '../types/interfaces'; class CancelError extends IrError { - constructor(message: string | undefined, config: IrRequestConfig, request?: any) { + constructor(message: string | undefined | null, config: IrRequestConfig, request?: any) { const errorMessage = message || 'canceled'; super(errorMessage, (IrError as any).ERR_CANCELED, config, request); this.name = 'CanceledError'; diff --git a/packages/inula-request/src/cancel/checkCancel.ts b/packages/inula-request/src/cancel/checkCancel.ts index 2fb875fb..8c2bafe7 100644 --- a/packages/inula-request/src/cancel/checkCancel.ts +++ b/packages/inula-request/src/cancel/checkCancel.ts @@ -15,7 +15,7 @@ // 检查是否为用户主动请求取消场景 function checkCancel(input: any): boolean { - return input.cancelFlag || false; + return input.name === 'CanceledError' || input.cancelFlag || false; } export default checkCancel; diff --git a/packages/inula-request/src/core/IrError.ts b/packages/inula-request/src/core/IrError.ts index 4f4f9605..fe9a2fb7 100644 --- a/packages/inula-request/src/core/IrError.ts +++ b/packages/inula-request/src/core/IrError.ts @@ -98,6 +98,7 @@ const errorTypes = [ 'ERR_CANCELED', 'ERR_NOT_SUPPORT', 'ERR_INVALID_URL', + 'ERR_FETCH_FAILED', ]; const descriptors: PropertyDescriptorMap = errorTypes.reduce((acc, code) => { diff --git a/packages/inula-request/src/request/fetchRequest.ts b/packages/inula-request/src/request/fetchRequest.ts index d1b6e9d5..12160bdd 100644 --- a/packages/inula-request/src/request/fetchRequest.ts +++ b/packages/inula-request/src/request/fetchRequest.ts @@ -19,6 +19,7 @@ import { IrRequestConfig, IrResponse, Cancel } from '../types/interfaces'; import { Method, ResponseType } from '../types/types'; import processUploadProgress from './processUploadProgress'; import processDownloadProgress from './processDownloadProgress'; +import CancelError from '../cancel/CancelError'; export const fetchRequest = (config: IrRequestConfig): Promise => { return new Promise((resolve, reject) => { @@ -47,8 +48,9 @@ export const fetchRequest = (config: IrRequestConfig): Promise => { // 处理请求取消 if (cancelToken) { cancelToken.promise.then((reason: Cancel) => { + const cancelError = new CancelError(reason.message, config); controller.abort(); - reject(reason); + reject(cancelError); }); } @@ -166,18 +168,17 @@ export const fetchRequest = (config: IrRequestConfig): Promise => { } }) .catch((error: IrError) => { - if (error.name === 'AbortError') { - reject(error.message); - } else { - reject(error); - } + const irError = new IrError(error.message, 'ERR_FETCH_FAILED', responseData.config, responseData.request, responseData); + reject(irError); }); }) .catch((error: IrError) => { if (error.name === 'AbortError') { - reject(error.message); + const cancelError = new CancelError('request canceled', config); + reject(cancelError); } else { - reject(error); + const irError = new IrError(error.message, 'ERR_FETCH_FAILED'); + reject(irError); } }); } diff --git a/packages/inula-request/src/request/processRequest.ts b/packages/inula-request/src/request/processRequest.ts index f904cdd1..ceeaa5e6 100644 --- a/packages/inula-request/src/request/processRequest.ts +++ b/packages/inula-request/src/request/processRequest.ts @@ -29,10 +29,6 @@ export default function processRequest(config: IrRequestConfig): Promise); @@ -53,6 +49,16 @@ export default function processRequest(config: IrRequestConfig): Promise { + if (config.signal?.aborted) { + error.response = { + data: null, + headers: config.headers, + status: 200, + statusText: 'ok', + config, + }; + } + if (!checkCancel(error)) { if (config.cancelToken) { config.cancelToken.throwIfRequested(); From 3b401b84f1c0cb7ec7349554329a7b5cf87206f4 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 31 Oct 2023 15:58:17 +0800 Subject: [PATCH 029/108] =?UTF-8?q?[inula-request]=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=AF=B9=E5=A4=96=E7=B1=BB=E5=9E=8B=20IrInstance=20?= =?UTF-8?q?=E5=92=8C=20IrProgressEvent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-request/index.ts | 2 +- .../src/request/processDownloadProgress.ts | 8 +++++++- .../src/request/processUploadProgress.ts | 6 +++--- packages/inula-request/src/types/interfaces.ts | 15 +++++++++++++++ 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/inula-request/index.ts b/packages/inula-request/index.ts index 464cdd8b..85ca6ff4 100644 --- a/packages/inula-request/index.ts +++ b/packages/inula-request/index.ts @@ -68,7 +68,7 @@ export { isAxiosError, }; -export type { IrRequestConfig, IrResponse, IrInstance, CancelTokenSource } from './src/types/interfaces'; +export type { IrRequestConfig, IrResponse, IrInstance, CancelTokenSource, IrProgressEvent } from './src/types/interfaces'; export type { Method, ResponseType } from './src/types/types'; export default inulaRequest; diff --git a/packages/inula-request/src/request/processDownloadProgress.ts b/packages/inula-request/src/request/processDownloadProgress.ts index 5e2ea808..58b211a3 100644 --- a/packages/inula-request/src/request/processDownloadProgress.ts +++ b/packages/inula-request/src/request/processDownloadProgress.ts @@ -13,7 +13,13 @@ * See the Mulan PSL v2 for more details. */ -function processDownloadProgress(stream: ReadableStream | null, response: Response, onProgress: Function | null) { +import { IrProgressEvent } from '../types/interfaces'; + +function processDownloadProgress( + stream: ReadableStream | null, + response: Response, + onProgress: (progressEvent: IrProgressEvent) => void | null +) { // 文件下载过程中更新进度 if (onProgress) { const reader = stream?.getReader(); diff --git a/packages/inula-request/src/request/processUploadProgress.ts b/packages/inula-request/src/request/processUploadProgress.ts index 4f5299a9..0ee4bea9 100644 --- a/packages/inula-request/src/request/processUploadProgress.ts +++ b/packages/inula-request/src/request/processUploadProgress.ts @@ -13,11 +13,11 @@ * See the Mulan PSL v2 for more details. */ -import { IrRequestConfig, IrResponse } from '../types/interfaces'; -import IrError from "../core/IrError"; +import { IrProgressEvent, IrRequestConfig, IrResponse } from '../types/interfaces'; +import IrError from '../core/IrError'; function processUploadProgress( - onUploadProgress: Function | null, + onUploadProgress: (progressEvent: IrProgressEvent) => void | null, data: FormData, reject: (reason?: any) => void, resolve: (value: PromiseLike> | IrResponse) => void, diff --git a/packages/inula-request/src/types/interfaces.ts b/packages/inula-request/src/types/interfaces.ts index 146cc773..ce4d2dc6 100644 --- a/packages/inula-request/src/types/interfaces.ts +++ b/packages/inula-request/src/types/interfaces.ts @@ -125,6 +125,9 @@ export interface IrInterface { // Ir 实例接口类型 export interface IrInstance extends IrInterface { + >(config: IrRequestConfig): Promise; + >(url: string, config?: IrRequestConfig): Promise; + // Ir 类 InulaRequest: IrInterface; @@ -177,6 +180,18 @@ export interface IrInstance extends IrInterface { AxiosHeaders: any; } +export interface IrProgressEvent { + loaded: string | number; + total?: string | number | null; + progress?: number; + bytes?: number; + rate?: number; + estimated?: number; + upload?: boolean; + download?: boolean; + event?: any; +} + export interface Interceptors { request: IrInterceptorManager; response: IrInterceptorManager; From bf813cecffd4c62ea4cf24d7d721853ad4079cc7 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 31 Oct 2023 17:51:59 +0800 Subject: [PATCH 030/108] =?UTF-8?q?[inula-dev-tools]=20=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E4=BD=8E=E7=89=88=E6=9C=AC=E6=B5=8F=E8=A7=88=E5=99=A8?= =?UTF-8?q?=E7=9A=84=E7=AA=97=E5=8F=A3=E7=9B=91=E5=90=AC=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-dev-tools/src/components/SizeObserver.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/inula-dev-tools/src/components/SizeObserver.tsx diff --git a/packages/inula-dev-tools/src/components/SizeObserver.tsx b/packages/inula-dev-tools/src/components/SizeObserver.tsx new file mode 100644 index 00000000..e69de29b From 8a562c44a22c49af7bb29b7bdc2f60c905d61453 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Wed, 1 Nov 2023 09:08:53 +0800 Subject: [PATCH 031/108] =?UTF-8?q?[inula-dev-tools]=20=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E4=BD=8E=E7=89=88=E6=9C=AC=E6=B5=8F=E8=A7=88=E5=99=A8?= =?UTF-8?q?=E7=9A=84=E7=AA=97=E5=8F=A3=E7=9B=91=E5=90=AC=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/resizeEvent.ts | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 packages/inula-dev-tools/src/components/resizeEvent.ts diff --git a/packages/inula-dev-tools/src/components/resizeEvent.ts b/packages/inula-dev-tools/src/components/resizeEvent.ts new file mode 100644 index 00000000..747faef3 --- /dev/null +++ b/packages/inula-dev-tools/src/components/resizeEvent.ts @@ -0,0 +1,95 @@ +/* + * 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. + */ + +/** + * + * 由于 ResizeObserver 对 IE 和低版本主流浏览器不兼容,需要自己实现一套兼容方案 + * 这是一个不依赖任何框架的监听 dom 元素尺寸变化的解决方案 + * 浏览器出于性能考虑,只有 window 的 resize 事件会触发,我们通过 object 标签可以得到 + * 一个 window 对象,让 object dom 元素成为待观测 dom 的子元素,并且和待观测 dom 大小一致。 + * 这样一旦待观测 dom 的大小发生变化,window 的大小也会发生变化,我们就可以通过监听 window + * 大小变化的方式监听待观测 dom 的大小变化 + * + *
Date: Wed, 1 Nov 2023 20:44:41 +0800 Subject: [PATCH 032/108] =?UTF-8?q?[inula-connectRouter]=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=94=AF=E6=8C=81inulax=E7=8A=B6=E6=80=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=99=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-router/src/connect-router/connectedRouter.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/inula-router/src/connect-router/connectedRouter.tsx b/packages/inula-router/src/connect-router/connectedRouter.tsx index a526663a..4845dca2 100644 --- a/packages/inula-router/src/connect-router/connectedRouter.tsx +++ b/packages/inula-router/src/connect-router/connectedRouter.tsx @@ -130,9 +130,14 @@ function getConnectedRouter(type: StoreType) { ); }; + const ConnectHRouterWithContext = (props: any) => { + const { store, ...rest } = props; + return ; + }; + // 针对不同的Store类型,使用对应的connect函数 if (type === 'InulaXCompat') { - return hConnect(null as any, mapDispatchToProps)(ConnectedRouterWithContext as any); + return hConnect(null as any, mapDispatchToProps)(ConnectHRouterWithContext as any); } if (type === 'Redux') { return connect(null, mapDispatchToProps)(ConnectedRouterWithContext); From 2d5de65ac000e37627bbe14c3f972f0e0f28d4f3 Mon Sep 17 00:00:00 2001 From: wangxinrong <18629193282@163.com> Date: Wed, 1 Nov 2023 21:02:10 +0800 Subject: [PATCH 033/108] =?UTF-8?q?[inula-package.json]=20exports=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/package.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/inula/package.json b/packages/inula/package.json index 3ec98d8b..38de80bf 100644 --- a/packages/inula/package.json +++ b/packages/inula/package.json @@ -24,5 +24,13 @@ "watch-test": "yarn test --watch --dev" }, "files": ["build/@types", "build/cjs", "build/umd", "build/index.js", "build/jsx-dev-runtime.js", "build/jsx-runtime.js", "README.md"], - "types": "./build/@types/index.d.ts" + "types": "./build/@types/index.d.ts", + "exports": { + ".": { + "default": "./index.js" + }, + "./package.json": "./package.json", + "./jsx-runtime": "./jsx-runtime.js", + "./jsx-dev-runtime": "./jsx-dev-runtime.js" + } } From 79d40a9b9d9858620b5d1f26ddc2731bc2d34177 Mon Sep 17 00:00:00 2001 From: wangxinrong <18629193282@163.com> Date: Wed, 1 Nov 2023 21:19:41 +0800 Subject: [PATCH 034/108] =?UTF-8?q?[inula-package.json]=20exports=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/package.json | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/inula/package.json b/packages/inula/package.json index 38de80bf..3ec98d8b 100644 --- a/packages/inula/package.json +++ b/packages/inula/package.json @@ -24,13 +24,5 @@ "watch-test": "yarn test --watch --dev" }, "files": ["build/@types", "build/cjs", "build/umd", "build/index.js", "build/jsx-dev-runtime.js", "build/jsx-runtime.js", "README.md"], - "types": "./build/@types/index.d.ts", - "exports": { - ".": { - "default": "./index.js" - }, - "./package.json": "./package.json", - "./jsx-runtime": "./jsx-runtime.js", - "./jsx-dev-runtime": "./jsx-dev-runtime.js" - } + "types": "./build/@types/index.d.ts" } From 08077af64f2e42a9aafa41f7b7030f251453419b Mon Sep 17 00:00:00 2001 From: wangxinrong <18629193282@163.com> Date: Wed, 1 Nov 2023 21:28:59 +0800 Subject: [PATCH 035/108] =?UTF-8?q?[inula-package.json]=20exports=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/package.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/inula/package.json b/packages/inula/package.json index 3ec98d8b..38de80bf 100644 --- a/packages/inula/package.json +++ b/packages/inula/package.json @@ -24,5 +24,13 @@ "watch-test": "yarn test --watch --dev" }, "files": ["build/@types", "build/cjs", "build/umd", "build/index.js", "build/jsx-dev-runtime.js", "build/jsx-runtime.js", "README.md"], - "types": "./build/@types/index.d.ts" + "types": "./build/@types/index.d.ts", + "exports": { + ".": { + "default": "./index.js" + }, + "./package.json": "./package.json", + "./jsx-runtime": "./jsx-runtime.js", + "./jsx-dev-runtime": "./jsx-dev-runtime.js" + } } From cc49467c2ff38db684435d830cd280577b74db70 Mon Sep 17 00:00:00 2001 From: wangxinrong <18629193282@163.com> Date: Wed, 1 Nov 2023 21:36:57 +0800 Subject: [PATCH 036/108] =?UTF-8?q?[inula-package.json]=20exports=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-router/src/connect-router/connectedRouter.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/inula-router/src/connect-router/connectedRouter.tsx b/packages/inula-router/src/connect-router/connectedRouter.tsx index 4845dca2..a526663a 100644 --- a/packages/inula-router/src/connect-router/connectedRouter.tsx +++ b/packages/inula-router/src/connect-router/connectedRouter.tsx @@ -130,14 +130,9 @@ function getConnectedRouter(type: StoreType) { ); }; - const ConnectHRouterWithContext = (props: any) => { - const { store, ...rest } = props; - return ; - }; - // 针对不同的Store类型,使用对应的connect函数 if (type === 'InulaXCompat') { - return hConnect(null as any, mapDispatchToProps)(ConnectHRouterWithContext as any); + return hConnect(null as any, mapDispatchToProps)(ConnectedRouterWithContext as any); } if (type === 'Redux') { return connect(null, mapDispatchToProps)(ConnectedRouterWithContext); From 0636d8dab05abd839e4bbbaae6ee45729531deed Mon Sep 17 00:00:00 2001 From: wangxinrong <18629193282@163.com> Date: Wed, 1 Nov 2023 21:44:43 +0800 Subject: [PATCH 037/108] =?UTF-8?q?[inulax]=20connect=20API=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81forwardRef=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-router/src/router/withRouter.tsx | 5 +++-- packages/inula/package.json | 10 +-------- .../inula/src/inulax/adapters/reduxReact.ts | 21 ++++++++++++++----- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/inula-router/src/router/withRouter.tsx b/packages/inula-router/src/router/withRouter.tsx index 3d7f58bd..43f7dc01 100644 --- a/packages/inula-router/src/router/withRouter.tsx +++ b/packages/inula-router/src/router/withRouter.tsx @@ -20,13 +20,14 @@ import RouterContext from './context'; function withRouter(Component: C) { function ComponentWithRouterProp(props: any) { + const { wrappedComponentRef, ...rest } = props; const { history, location, match } = useContext(RouterContext); const routeProps = { history: history, location: location, match: match }; - return ; + return ; } return ComponentWithRouterProp; } -export default withRouter; \ No newline at end of file +export default withRouter; diff --git a/packages/inula/package.json b/packages/inula/package.json index 38de80bf..3ec98d8b 100644 --- a/packages/inula/package.json +++ b/packages/inula/package.json @@ -24,13 +24,5 @@ "watch-test": "yarn test --watch --dev" }, "files": ["build/@types", "build/cjs", "build/umd", "build/index.js", "build/jsx-dev-runtime.js", "build/jsx-runtime.js", "README.md"], - "types": "./build/@types/index.d.ts", - "exports": { - ".": { - "default": "./index.js" - }, - "./package.json": "./package.json", - "./jsx-runtime": "./jsx-runtime.js", - "./jsx-dev-runtime": "./jsx-dev-runtime.js" - } + "types": "./build/@types/index.d.ts" } diff --git a/packages/inula/src/inulax/adapters/reduxReact.ts b/packages/inula/src/inulax/adapters/reduxReact.ts index 51650f98..203622f3 100644 --- a/packages/inula/src/inulax/adapters/reduxReact.ts +++ b/packages/inula/src/inulax/adapters/reduxReact.ts @@ -17,6 +17,7 @@ import { useState, useContext, useEffect, useRef } from '../../renderer/hooks/Ho import { createContext } from '../../renderer/components/context/CreateContext'; import { createElement } from '../../external/JSXElement'; import type { ReduxStoreHandler, ReduxAction, BoundActionCreator } from './redux'; +import { forwardRef } from '../../renderer/components/ForwardRef'; const DefaultContext = createContext(null); type Context = typeof DefaultContext; @@ -90,6 +91,11 @@ type MergePropsP = ( type WrappedComponent = (props: OwnProps) => ReturnType; type OriginalComponent = (props: MergedProps) => ReturnType; type Connector = (Component: OriginalComponent) => WrappedComponent; +type ConnectOption = { + areStatesEqual?: (oldState: State, newState: State) => boolean; + context?: Context; + forwardRef?: boolean +} export function connect( mapStateToProps: MapStateToPropsP = () => ({} as StateProps), @@ -97,12 +103,9 @@ export function connect( mergeProps: MergePropsP = ( stateProps, dispatchProps, - ownProps + ownProps, ): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps } as unknown as MergedProps), - options?: { - areStatesEqual?: (oldState: any, newState: any) => boolean; - context?: Context; - } + options: ConnectOption, ): Connector { if (!options) { options = {}; @@ -159,6 +162,7 @@ export function connect( mappedDispatch = mapDispatchToProps(store.dispatch, props); } } + mappedDispatch = Object.assign({}, mappedDispatch, { dispatch: store.dispatch }); const mergedProps = ( mergeProps || ((state, dispatch, originalProps) => { @@ -172,6 +176,13 @@ export function connect( return node; }; + if (options.forwardRef) { + const forwarded = forwardRef(function (props, ref) { + return Wrapper({ ...props, ref: ref }); + }); + return forwarded as WrappedComponent; + } + return Wrapper; }; } From 628e09fe20d0d3ce601f78b8d297ea974c75baec Mon Sep 17 00:00:00 2001 From: wangxinrong <18629193282@163.com> Date: Wed, 1 Nov 2023 21:51:14 +0800 Subject: [PATCH 038/108] =?UTF-8?q?[inulax]=20history=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-router/src/history/hashHistory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/inula-router/src/history/hashHistory.ts b/packages/inula-router/src/history/hashHistory.ts index c44a85bb..f1cf421d 100644 --- a/packages/inula-router/src/history/hashHistory.ts +++ b/packages/inula-router/src/history/hashHistory.ts @@ -107,7 +107,7 @@ export function createHashHistory(option: HashHistoryOptio warning(state !== undefined, 'Hash history does not support state, it will be ignored'); const action = Action.push; - const location = createLocation(history.location, to, undefined, ''); + const location = createLocation(history.location, to, state, ''); transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => { if (!isJump) { @@ -132,7 +132,7 @@ export function createHashHistory(option: HashHistoryOptio function replace(to: To, state?: S) { warning(state !== undefined, 'Hash history does not support state, it will be ignored'); const action = Action.replace; - const location = createLocation(history.location, to, undefined, ''); + const location = createLocation(history.location, to, state, ''); transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => { if (!isJump) { From 8c0372803c2229b5f88b8924c07fbb03c262da51 Mon Sep 17 00:00:00 2001 From: wangxinrong <18629193282@163.com> Date: Wed, 1 Nov 2023 21:56:49 +0800 Subject: [PATCH 039/108] =?UTF-8?q?[inulax]=20history=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-router/src/router/withRouter.tsx | 5 ++--- .../inula/src/inulax/adapters/reduxReact.ts | 21 +++++-------------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/packages/inula-router/src/router/withRouter.tsx b/packages/inula-router/src/router/withRouter.tsx index 43f7dc01..3d7f58bd 100644 --- a/packages/inula-router/src/router/withRouter.tsx +++ b/packages/inula-router/src/router/withRouter.tsx @@ -20,14 +20,13 @@ import RouterContext from './context'; function withRouter(Component: C) { function ComponentWithRouterProp(props: any) { - const { wrappedComponentRef, ...rest } = props; const { history, location, match } = useContext(RouterContext); const routeProps = { history: history, location: location, match: match }; - return ; + return ; } return ComponentWithRouterProp; } -export default withRouter; +export default withRouter; \ No newline at end of file diff --git a/packages/inula/src/inulax/adapters/reduxReact.ts b/packages/inula/src/inulax/adapters/reduxReact.ts index 203622f3..51650f98 100644 --- a/packages/inula/src/inulax/adapters/reduxReact.ts +++ b/packages/inula/src/inulax/adapters/reduxReact.ts @@ -17,7 +17,6 @@ import { useState, useContext, useEffect, useRef } from '../../renderer/hooks/Ho import { createContext } from '../../renderer/components/context/CreateContext'; import { createElement } from '../../external/JSXElement'; import type { ReduxStoreHandler, ReduxAction, BoundActionCreator } from './redux'; -import { forwardRef } from '../../renderer/components/ForwardRef'; const DefaultContext = createContext(null); type Context = typeof DefaultContext; @@ -91,11 +90,6 @@ type MergePropsP = ( type WrappedComponent = (props: OwnProps) => ReturnType; type OriginalComponent = (props: MergedProps) => ReturnType; type Connector = (Component: OriginalComponent) => WrappedComponent; -type ConnectOption = { - areStatesEqual?: (oldState: State, newState: State) => boolean; - context?: Context; - forwardRef?: boolean -} export function connect( mapStateToProps: MapStateToPropsP = () => ({} as StateProps), @@ -103,9 +97,12 @@ export function connect( mergeProps: MergePropsP = ( stateProps, dispatchProps, - ownProps, + ownProps ): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps } as unknown as MergedProps), - options: ConnectOption, + options?: { + areStatesEqual?: (oldState: any, newState: any) => boolean; + context?: Context; + } ): Connector { if (!options) { options = {}; @@ -162,7 +159,6 @@ export function connect( mappedDispatch = mapDispatchToProps(store.dispatch, props); } } - mappedDispatch = Object.assign({}, mappedDispatch, { dispatch: store.dispatch }); const mergedProps = ( mergeProps || ((state, dispatch, originalProps) => { @@ -176,13 +172,6 @@ export function connect( return node; }; - if (options.forwardRef) { - const forwarded = forwardRef(function (props, ref) { - return Wrapper({ ...props, ref: ref }); - }); - return forwarded as WrappedComponent; - } - return Wrapper; }; } From a907240e58963654998a77aa265b77e4b07ccc63 Mon Sep 17 00:00:00 2001 From: wangxinrong <18629193282@163.com> Date: Wed, 1 Nov 2023 22:00:57 +0800 Subject: [PATCH 040/108] =?UTF-8?q?[inulax]=20history=E5=93=88=E5=B8=8C?= =?UTF-8?q?=E5=80=BC=E5=92=8Csearch=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-router/src/history/utils.ts | 43 +++++++++++++--------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/inula-router/src/history/utils.ts b/packages/inula-router/src/history/utils.ts index cfa49b7a..27447967 100644 --- a/packages/inula-router/src/history/utils.ts +++ b/packages/inula-router/src/history/utils.ts @@ -28,31 +28,32 @@ export function createPath(path: Partial): string { } export function parsePath(url: string): Partial { - if (!url) { - return {}; - } - let parsedPath: Partial = {}; + const parsedPath: Partial = { + pathname: url || '/', + search: '', + hash: '', + }; - let hashIdx = url.indexOf('#'); + const hashIdx = url.indexOf('#'); if (hashIdx > -1) { - parsedPath.hash = url.substring(hashIdx); + const hash = url.substring(hashIdx); + parsedPath.hash = hash === '#' ? '' : hash; url = url.substring(0, hashIdx); } - let searchIdx = url.indexOf('?'); + const searchIdx = url.indexOf('?'); if (searchIdx > -1) { - parsedPath.search = url.substring(searchIdx); + const search = url.substring(searchIdx); + parsedPath.search = search === '?' ? '' : search; url = url.substring(0, searchIdx); } - if (url) { - parsedPath.pathname = url; - } + parsedPath.pathname = url; return parsedPath; } export function createLocation(current: string | Location, to: To, state?: S, key?: string): Readonly> { - let pathname = typeof current === 'string' ? current : current.pathname; - let urlObj = typeof to === 'string' ? parsePath(to) : to; + const pathname = typeof current === 'string' ? current : current.pathname; + const urlObj = typeof to === 'string' ? parsePath(to) : to; // 随机key长度取6 const getRandKey = genRandomKey(6); const location = { @@ -64,7 +65,13 @@ export function createLocation(current: string | Location, to: To, state?: S, ...urlObj, }; if (!location.pathname) { - location.pathname = '/'; + location.pathname = pathname ? pathname : '/'; + } + if (location.search && location.search[0] !== '?') { + location.search = '?' + location.search; + } + if (location.hash && location.hash[0] !== '#') { + location.hash = '#' + location.hash; } return location; } @@ -95,7 +102,7 @@ export function normalizeSlash(path: string): string { return tempPath; } -export function hasBasename(path: string, prefix: string): Boolean { +export function hasBasename(path: string, prefix: string): boolean { return ( path.toLowerCase().indexOf(prefix.toLowerCase()) === 0 && ['/', '?', '#', ''].includes(path.charAt(prefix.length)) ); @@ -109,12 +116,12 @@ export function stripBasename(path: string, prefix: string): string { export function createMemoryRecord(initVal: S, fn: (arg: S) => T) { let visitedRecord: T[] = [fn(initVal)]; - function getDelta(toKey: S, fromKey: S): number { - let toIdx = visitedRecord.lastIndexOf(fn(toKey)); + function getDelta(to: S, form: S): number { + let toIdx = visitedRecord.lastIndexOf(fn(to)); if (toIdx === -1) { toIdx = 0; } - let fromIdx = visitedRecord.lastIndexOf(fn(fromKey)); + let fromIdx = visitedRecord.lastIndexOf(fn(form)); if (fromIdx === -1) { fromIdx = 0; } From d6e105e9c4d7526d47e157902946d6b40a912443 Mon Sep 17 00:00:00 2001 From: wangxinrong <18629193282@163.com> Date: Wed, 1 Nov 2023 22:04:46 +0800 Subject: [PATCH 041/108] =?UTF-8?q?[inulax]=20history=E5=93=88=E5=B8=8C?= =?UTF-8?q?=E5=80=BC=E5=92=8Csearch=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-router/src/history/hashHistory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/inula-router/src/history/hashHistory.ts b/packages/inula-router/src/history/hashHistory.ts index f1cf421d..c44a85bb 100644 --- a/packages/inula-router/src/history/hashHistory.ts +++ b/packages/inula-router/src/history/hashHistory.ts @@ -107,7 +107,7 @@ export function createHashHistory(option: HashHistoryOptio warning(state !== undefined, 'Hash history does not support state, it will be ignored'); const action = Action.push; - const location = createLocation(history.location, to, state, ''); + const location = createLocation(history.location, to, undefined, ''); transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => { if (!isJump) { @@ -132,7 +132,7 @@ export function createHashHistory(option: HashHistoryOptio function replace(to: To, state?: S) { warning(state !== undefined, 'Hash history does not support state, it will be ignored'); const action = Action.replace; - const location = createLocation(history.location, to, state, ''); + const location = createLocation(history.location, to, undefined, ''); transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => { if (!isJump) { From 9bcc0b05ee8378e8c37f59288049e81975a59ec0 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Thu, 2 Nov 2023 11:04:18 +0800 Subject: [PATCH 042/108] =?UTF-8?q?[inula-dev-tools]=20Search=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/Search.less | 18 +++++++ .../inula-dev-tools/src/components/Search.tsx | 44 +++++++++++++++++ .../src/components/SizeObserver.tsx | 48 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 packages/inula-dev-tools/src/components/Search.less create mode 100644 packages/inula-dev-tools/src/components/Search.tsx diff --git a/packages/inula-dev-tools/src/components/Search.less b/packages/inula-dev-tools/src/components/Search.less new file mode 100644 index 00000000..72752465 --- /dev/null +++ b/packages/inula-dev-tools/src/components/Search.less @@ -0,0 +1,18 @@ +/* + * 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. + */ + +.search { + width: 100%; +} diff --git a/packages/inula-dev-tools/src/components/Search.tsx b/packages/inula-dev-tools/src/components/Search.tsx new file mode 100644 index 00000000..ea1b48c8 --- /dev/null +++ b/packages/inula-dev-tools/src/components/Search.tsx @@ -0,0 +1,44 @@ +/* + * 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 styles from './Search.less'; + +interface SearchProps { + onKeyUp: () => void; + onChange: (event: any) => void; + value: string; +} + +export default function Search(props: SearchProps) { + const { onChange, value, onKeyUp } = props; + const handleChange = event => { + onChange(event.target.value); + }; + const handleKeyUp = event => { + if (event.key === 'Enter') { + onKeyUp(); + } + }; + + return ( + + ); +} diff --git a/packages/inula-dev-tools/src/components/SizeObserver.tsx b/packages/inula-dev-tools/src/components/SizeObserver.tsx index e69de29b..32d92058 100644 --- a/packages/inula-dev-tools/src/components/SizeObserver.tsx +++ b/packages/inula-dev-tools/src/components/SizeObserver.tsx @@ -0,0 +1,48 @@ +/* + * 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 { useEffect, useState, useRef } from 'openinula'; +import { addResizeListener, removeResizeListener } from './resizeEvent'; + +export function SizeObserver(props) { + const { children, ...rest } = props; + const containerRef = useRef(); + const [size, setSize] = useState<{ width: number; height: number }>(); + const notifyChild = (element) => { + setSize({ + width: element.offsetWidth, + height: element.offsetHeight, + }); + }; + + useEffect(() => { + const element = containerRef.current!; + setSize({ + width: element.offsetWidth, + height: element.offsetHeight, + }); + addResizeListener(element, notifyChild); + return () => { + removeResizeListener(element, notifyChild); + }; + }, []); + const myChild = size ? children(size.width, size.height) : null; + + return ( +
+ {myChild} +
+ ); +} From 8292aa54439bf79477a865fe061d5db3d4eb015f Mon Sep 17 00:00:00 2001 From: yangxuting Date: Thu, 2 Nov 2023 15:01:11 +0800 Subject: [PATCH 043/108] =?UTF-8?q?[create-inula]=20Simple-app=20webpack?= =?UTF-8?q?=20=E6=A8=A1=E6=9D=BF=E6=B7=BB=E5=8A=A0jsx=E7=AD=89=E5=90=8E?= =?UTF-8?q?=E7=BC=80=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../generators/Simple-app/templates/webpack/webpack.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/create-inula/lib/generators/Simple-app/templates/webpack/webpack.config.js b/packages/create-inula/lib/generators/Simple-app/templates/webpack/webpack.config.js index 10c148b9..9056a484 100644 --- a/packages/create-inula/lib/generators/Simple-app/templates/webpack/webpack.config.js +++ b/packages/create-inula/lib/generators/Simple-app/templates/webpack/webpack.config.js @@ -78,4 +78,7 @@ module.exports = { port: 9000, open: true, }, + resolve: { + extensions: ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'], + }, }; From cc9c10b84bc0daf395d555400ed8c640172121a9 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Fri, 3 Nov 2023 17:57:43 +0800 Subject: [PATCH 044/108] =?UTF-8?q?[inula-dev-tools]=20=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=20tsconfig=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-dev-tools/tsconfig.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/inula-dev-tools/tsconfig.json b/packages/inula-dev-tools/tsconfig.json index 2e1cc0d6..d0c62d99 100644 --- a/packages/inula-dev-tools/tsconfig.json +++ b/packages/inula-dev-tools/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { "outDir": "./dist", - "incremental": false, "allowJs": true, "strict": true, "noImplicitAny": false, @@ -17,6 +16,6 @@ } }, "include": [ - "./src/*/*.ts", "./src/index.d.ts", "./src/*/*.tsx" + "./src/**/*.ts", "./src/index.d.ts", "./src/**/*.tsx", "./externals.d.ts" ] } From ee52d818dc8711c39bbc49fb56f729ef497e7013 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 7 Nov 2023 10:56:44 +0800 Subject: [PATCH 045/108] =?UTF-8?q?[inula-dev-tools]=20=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=88=97=E8=A1=A8=E7=BB=84=E4=BB=B6=E5=90=88=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/VList/ItemMap.ts | 80 +++++++++++ .../src/components/VList/VList.less | 26 ++++ .../src/components/VList/VList.tsx | 135 ++++++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 packages/inula-dev-tools/src/components/VList/ItemMap.ts create mode 100644 packages/inula-dev-tools/src/components/VList/VList.less create mode 100644 packages/inula-dev-tools/src/components/VList/VList.tsx diff --git a/packages/inula-dev-tools/src/components/VList/ItemMap.ts b/packages/inula-dev-tools/src/components/VList/ItemMap.ts new file mode 100644 index 00000000..81109df4 --- /dev/null +++ b/packages/inula-dev-tools/src/components/VList/ItemMap.ts @@ -0,0 +1,80 @@ +/* + * 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. + */ + +/** + * 用于在滚动的过程中,对比上一次渲染的结果和本次需要渲染项 + * 确保继续渲染项在新渲染数组中的位置和旧渲染数组中的位置不发生改变 + */ +export default class ItemMap { + + // 不要用 indexOf 进行位置计算,它会遍历数组 + private lastRenderItemToIndexMap: Map; + + constructor() { + this.lastRenderItemToIndexMap = new Map(); + } + + public calculateReSortedItems(nextItems: T[]): (T|undefined)[] { + if (this.lastRenderItemToIndexMap.size === 0) { + nextItems.forEach((item, index) => { + this.lastRenderItemToIndexMap.set(item, index); + }); + return nextItems; + } + + const nextRenderItems: T[] = []; + const length = nextItems.length; + const nextRenderItemToIndexMap = new Map(); + const addItems = []; + + // 遍历 nextItems 找到复用 item 和新增 item + nextItems.forEach(item => { + const lastIndex = this.lastRenderItemToIndexMap.get(item); + // 处理旧 item + if (lastIndex !== undefined) { + // 使用上一次的位置 + nextRenderItems[lastIndex] = item; + // 记录位置 + nextRenderItemToIndexMap.set(item, lastIndex); + } else { + // 记录新的 item + addItems.push(item); + } + }); + + // 处理新增 item,翻转数组,后面在调用 pop 时拿到的时最后一个,以确保顺序 + addItems.reverse(); + for (let i = 0; i < length; i++) { + // 优先将新增 item 放置在空位置上 + if (!nextRenderItems[i]) { + const item = addItems.pop(); + nextRenderItems[i] = item; + nextRenderItemToIndexMap.set(item, i); + } + } + + // 剩余新 item 补在数组后面 + for (let i = addItems.length - 1; i >= 0; i--) { + const item = addItems[i]; + nextRenderItemToIndexMap.set(item, nextRenderItems.length); + nextRenderItems.push(item); + } + + // 如果 nextRenderItems 中存在空 index,nextItems 已经耗尽,不用处理 + // 确保新旧数组中 item 的 index 值不会发生变化 + this.lastRenderItemToIndexMap = nextRenderItemToIndexMap; + return nextRenderItems; + } +} diff --git a/packages/inula-dev-tools/src/components/VList/VList.less b/packages/inula-dev-tools/src/components/VList/VList.less new file mode 100644 index 00000000..27d47999 --- /dev/null +++ b/packages/inula-dev-tools/src/components/VList/VList.less @@ -0,0 +1,26 @@ +/* + * 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. + */ + +.container { + position: relative; + overflow-y: auto; + height: 100%; + width: 100%; +} + +.item { + position: absolute; + width: 100%; +} diff --git a/packages/inula-dev-tools/src/components/VList/VList.tsx b/packages/inula-dev-tools/src/components/VList/VList.tsx new file mode 100644 index 00000000..86c148c3 --- /dev/null +++ b/packages/inula-dev-tools/src/components/VList/VList.tsx @@ -0,0 +1,135 @@ +/* + * 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. + */ + +/** + * 内部只记录滚动位置状态值 + * data 数组更新后不修改滚动位置,只有修改 scrollToItem 才会修改滚动位置 + */ +import { useState, useRef, useEffect, useMemo } from 'openinula'; +import styles from './VList.less'; +import ItemMap from './ItemMap'; +import { debounceFunc } from '../../utils/publicUtil'; + +interface IProps { + data: T[]; + maxDeep: number; + width: number; // 暂时未用到,当需要支持横向滚动时使用 + height: number; // VList 的高度 + children?: any; // inula 组件 + itemHeight: number; + scrollToItem?: T; // 滚动到指定项位置,如果该项在可见区域内,不滚动,如果补在,则滚动到中间位置 + onRendered: (renderInfo: RenderInfoType) => void; + filter?: (data: T) => boolean; // false 表示该行不显示 +} + +export type RenderInfoType = { + visibleItems: T[]; +} + +function parseTranslate(data: T[], itemHeight: number) { + const map = new Map(); + data.forEach((item, index) => { + map.set(item, index * itemHeight); + }) + return map; +} + +export function VList(props: IProps) { + const { data, maxDeep, height, width, children, itemHeight, scrollToItem, onRendered } = props; + const [scrollTop, setScrollTop] = useState(Math.max(data.indexOf(scrollToItem), 0) * itemHeight); + const renderInfoRef: { current: RenderInfoType } = useRef({ + visibleItems: [], + }); + const [indentationLength, setIndentationLength] = useState(0); + + // 每个 item 的 translateY 值固定不变 + const itemToTranslateYMap = useMemo(() => parseTranslate(data, itemHeight), [data]); + const itemIndexMap = useMemo(() => new ItemMap(), []); + const containerRef = useRef(); + + useEffect(() => { + onRendered(renderInfoRef.current); + }); + + useEffect(() => { + debounceFunc(() => setIndentationLength(Math.min(12, Math.round(width / (2 * maxDeep))))); + }, [width]); + + useEffect(() => { + if (scrollToItem) { + const renderInfo = renderInfoRef.current; + // 在显示区域,不滚动 + if (!renderInfo.visibleItems.includes(scrollToItem)) { + const index = data.indexOf(scrollToItem); + // 显示在页面中间 + const top = Math.max(index * itemHeight - height / 2, 0); + containerRef.current.scrollTo({ top: top }); + } + } + }, [scrollToItem]); + + // 滚动事件会频繁触发,通过框架提供的代理会有大量计算寻找 dom 元素,直接绑定到原生事件上减少计算量 + useEffect(() => { + const handleScroll = event => { + const scrollTop = event.target.scrollTop; + setScrollTop(scrollTop); + }; + const container = containerRef.current; + container.addEventListener('scroll', handleScroll); + return () => { + container.removeEventListener('scroll', handleScroll); + }; + }, []); + + const totalHeight = itemHeight * data.length; + const maxIndex = data.length; // slice 截取渲染 item 数组时最大位置不能超过自然长度 + // 第一个可见 item index + const firstInViewItemIndex = Math.floor(scrollTop / itemHeight); + // 可见区域前最多冗余 4 个 item + const startRenderIndex = Math.max(firstInViewItemIndex - 4, 0); // index 不能小于 0 + // 最多可见数量 + const maxInViewCount = Math.floor(height / itemHeight); + // 最后可见 item index + const lastInViewIndex = Math.min(firstInViewItemIndex + maxInViewCount, maxIndex); + // 记录可见 items + renderInfoRef.current.visibleItems = data.slice(firstInViewItemIndex, lastInViewIndex); + // 可见区域后冗余 4 个 item + const lastRenderIndex = Math.min(lastInViewIndex + 4, maxIndex); + // 需要渲染的 item + const renderItems = data.slice(startRenderIndex, lastRenderIndex); + // 给 items 重新排序,确保未移出渲染数组的 item 在新的渲染数组中位置不变,这样在 diff 算法比较后,这部分的 dom 不会发生更新 + const nextRenderList = itemIndexMap.calculateReSortedItems(renderItems); + const list = nextRenderList.map((item, index) => { + if (!item) { + return null; + } + return ( +
+ {children(item,indentationLength)} +
+ ); + }); + + return ( +
+ {list} +
+
+ ); +} From 527c5023988589296792dc7b3d6d200eec14a333 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 7 Nov 2023 11:06:56 +0800 Subject: [PATCH 046/108] =?UTF-8?q?[inula-dev-tools]=20=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=88=97=E8=A1=A8=E7=BB=84=E4=BB=B6=E5=90=88=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/VList/ItemMap.ts | 8 ++++---- .../src/components/VList/VList.tsx | 10 +++++----- .../src/components/VList/index.ts | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 packages/inula-dev-tools/src/components/VList/index.ts diff --git a/packages/inula-dev-tools/src/components/VList/ItemMap.ts b/packages/inula-dev-tools/src/components/VList/ItemMap.ts index 81109df4..e4c43bc3 100644 --- a/packages/inula-dev-tools/src/components/VList/ItemMap.ts +++ b/packages/inula-dev-tools/src/components/VList/ItemMap.ts @@ -20,7 +20,7 @@ export default class ItemMap { // 不要用 indexOf 进行位置计算,它会遍历数组 - private lastRenderItemToIndexMap: Map; + private lastRenderItemToIndexMap: Map; constructor() { this.lastRenderItemToIndexMap = new Map(); @@ -34,10 +34,10 @@ export default class ItemMap { return nextItems; } - const nextRenderItems: T[] = []; + const nextRenderItems: (T | undefined)[] = []; const length = nextItems.length; - const nextRenderItemToIndexMap = new Map(); - const addItems = []; + const nextRenderItemToIndexMap = new Map(); + const addItems: T[] = []; // 遍历 nextItems 找到复用 item 和新增 item nextItems.forEach(item => { diff --git a/packages/inula-dev-tools/src/components/VList/VList.tsx b/packages/inula-dev-tools/src/components/VList/VList.tsx index 86c148c3..9efbbf89 100644 --- a/packages/inula-dev-tools/src/components/VList/VList.tsx +++ b/packages/inula-dev-tools/src/components/VList/VList.tsx @@ -48,7 +48,7 @@ function parseTranslate(data: T[], itemHeight: number) { export function VList(props: IProps) { const { data, maxDeep, height, width, children, itemHeight, scrollToItem, onRendered } = props; - const [scrollTop, setScrollTop] = useState(Math.max(data.indexOf(scrollToItem), 0) * itemHeight); + const [scrollTop, setScrollTop] = useState(Math.max(data.indexOf(scrollToItem!), 0) * itemHeight); const renderInfoRef: { current: RenderInfoType } = useRef({ visibleItems: [], }); @@ -75,7 +75,7 @@ export function VList(props: IProps) { const index = data.indexOf(scrollToItem); // 显示在页面中间 const top = Math.max(index * itemHeight - height / 2, 0); - containerRef.current.scrollTo({ top: top }); + containerRef.current?.scrollTo({ top: top }); } } }, [scrollToItem]); @@ -87,9 +87,9 @@ export function VList(props: IProps) { setScrollTop(scrollTop); }; const container = containerRef.current; - container.addEventListener('scroll', handleScroll); + container?.addEventListener('scroll', handleScroll); return () => { - container.removeEventListener('scroll', handleScroll); + container?.removeEventListener('scroll', handleScroll); }; }, []); @@ -117,7 +117,7 @@ export function VList(props: IProps) { } return (
diff --git a/packages/inula-dev-tools/src/components/VList/index.ts b/packages/inula-dev-tools/src/components/VList/index.ts new file mode 100644 index 00000000..944e8eb8 --- /dev/null +++ b/packages/inula-dev-tools/src/components/VList/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { VList } from './VList'; +export type { RenderInfoType } from './VList'; From 80c4282898c4398db4b264ebb1ed42e814e187c3 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 7 Nov 2023 18:48:32 +0800 Subject: [PATCH 047/108] =?UTF-8?q?[inula-dev-tools]=20=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E8=A7=A3=E6=9E=90=E6=96=B9=E6=B3=95=E5=90=88=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-dev-tools/src/parser/parseAttr.ts | 418 ++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 packages/inula-dev-tools/src/parser/parseAttr.ts diff --git a/packages/inula-dev-tools/src/parser/parseAttr.ts b/packages/inula-dev-tools/src/parser/parseAttr.ts new file mode 100644 index 00000000..94473576 --- /dev/null +++ b/packages/inula-dev-tools/src/parser/parseAttr.ts @@ -0,0 +1,418 @@ +/* + * 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 { Hook } from '../../../inula/src/renderer/hooks/HookType'; +import { ModifyHooks, ModifyProps, ModifyState } from '../utils/constants'; +import { VNode } from '../../../inula/src/renderer/vnode/VNode'; +import { + ClassComponent, + FunctionComponent, + ContextConsumer, + ContextProvider, + ForwardRef, + SuspenseComponent, + MemoComponent, +} from '../../../inula/src/renderer/vnode/VNodeTags'; +import { helper } from '../injector'; +import { JSXElement, ContextType } from '../../../inula/src/renderer/Types'; +import { decycle } from 'json-decycle'; +import {arrify} from "ts-loader/dist/utils"; +import {add} from "../../../inula/src/renderer/taskExecutor/TaskQueue"; + +// 展示值为 string 的可编辑模型 +type EditableStringType = 'string' | 'number' | 'undefined' | 'null'; +// 展示值为 string 的不可编辑类型 +type UnEditableStringType = + | 'function' + | 'symbol' + | 'object' + | 'map' + | 'set' + | 'array' + | 'dom' // 值为 dom 元素的 ref 类型 + | 'ref'; // 值为其他数据的 ref 类型 + +type ShowAsStringType = EditableStringType | UnEditableStringType; + +export type IAttr = { + name: string | number; + indentation: number; + hIndex?: number; // 用于记录 hook 的 hIndex 值 +} & ( + | { + type: ShowAsStringType; + value: string; +} + | { + type: 'boolean'; + value: boolean; +} + ); + +type ShowType = ShowAsStringType | 'boolean'; + +const propsAndStateTag = [ClassComponent]; +const propsAndHooksTag = [FunctionComponent, ForwardRef]; +const propsTag = [ContextConsumer, ContextProvider, SuspenseComponent, MemoComponent]; +const MAX_TITLE_LENGTH = 50; + +function isJSXElement(obj: any): obj is JSXElement { + return !!(obj?.type && obj.vtype); +} + +const isCycle = (obj: any): boolean => { + return obj?.Consumer === obj; +}; + +const getObjectKeys = (attr: Record): Array => { + const keys: (string | symbol)[] = []; + let current = attr; + try { + while (current != null) { + const currentKeys = [ + ...Object.keys(current), + ...Object.getOwnPropertySymbols(current) + ]; + const descriptors = Object.getOwnPropertyDescriptors(current); + currentKeys.forEach(key => { + // @ts-ignore key 可以为 symbol 类型 + if (descriptors[key].enumerable) { + keys.push(key); + } + }); + current = Object.getPrototypeOf(current); + } + } catch (e) { + console.log(attr); + } + return keys; +}; + +// 用于比较两个 key 值的顺序 +export function sortKeys( + firstKey: string | number | symbol, + secondKey: string | number | symbol +): number { + if (firstKey.toString() > secondKey.toString()) { + return 1; + } else if (secondKey.toString() > firstKey.toString()) { + return -1; + } else { + return 0; + } +} + +const parseSubTitle = (attr: T) => { + const AttrType = typeof attr; + + if (Array.isArray(attr)) { + let title = ''; + // 当 i > 0 时多加一个逗号和空格,例如:Person: { name: 'XXX', age: xxx } + for (let i = 0; i < attr.length; i++) { + if (i > 0) { + title = `${title}, `; + } + title = `${title}${parseSubTitle(attr[i])}`; + if (title.length > MAX_TITLE_LENGTH) { + break; + } + } + + if (title.length > MAX_TITLE_LENGTH) { + title = `${title.substr(0, MAX_TITLE_LENGTH)}…`; + } + return `[${title}]`; + } else if (AttrType === 'string') { + return `"${attr}"`; + } else if (AttrType === 'function') { + const funcName = attr['name']; + return `ƒ ${funcName}() {}`; + } else if ( + AttrType === 'boolean' || + AttrType === 'number' || + AttrType === 'undefined' + ) { + return `${attr}`; + } else if (AttrType === 'object') { + if (attr === null) { + return 'null'; + } + + if (isCycle(attr)) { + attr = JSON.parse(JSON.stringify(attr, decycle())); + } + const keys = getObjectKeys(attr).sort(sortKeys); + let title = ''; + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + // 当 i > 0 时多加一个逗号和空格,例如:Person: { name: "xxx", age: xxx } + if (i > 0) { + title = `${title}, `; + } + title = `${title}${key.toString()}: ${parseSubTitle(attr[key])}`; + if (title.length > MAX_TITLE_LENGTH) { + break; + } + } + if (title.length > MAX_TITLE_LENGTH) { + title = `${title.substr(0, MAX_TITLE_LENGTH)}…`; + } + return `{${title}}`; + } else if (isJSXElement(attr)) { + let title = ''; + if (typeof attr.type === 'string') { + title = attr.type; + } else { + title = attr.type?.name ? attr.type.name : helper.getElementTag(attr); + } + return `${title} />`; + } +}; + +const parseSubAttr = ( + attr: any, + parentIndentation: number, + attrName: string, + result: IAttr[], + hIndex?: number +) => { + const AttrType = typeof attr; + let value: any; + let showType: any; + let addSubState; + + if ( + AttrType === 'boolean' || + AttrType === 'number' || + AttrType === 'undefined' || + AttrType === 'string' + ) { + value = attr; + showType = AttrType; + } else if (AttrType === 'function') { + const funcName = attr.name; + value = `ƒ ${funcName}() {}`; + } else if (AttrType === 'symbol') { + value = attr.description; + } else if (AttrType === 'object') { + if (attr === null) { + value = 'null'; + } else if (attr instanceof Map) { + showType = 'map'; + const size = attr.size; + value = `Map(${size})`; + addSubState = () => { + attr.forEach((value, key) => { + parseSubAttr(value, parentIndentation + 2, key, result); + }); + }; + } else if (attr instanceof Set) { + showType = 'set'; + const size = attr.size; + value = `Set(${size})`; + addSubState = () => { + let i = 0; + attr.forEach(value => { + parseSubAttr(value, parentIndentation + 2, String(i), result); + }); + i++; + }; + } else if (Array.isArray(attr)) { + showType = 'array'; + value = parseSubTitle(attr); + addSubState = () => { + attr.forEach((attrValue, index) => { + if (isJSXElement(attrValue)) { + if (typeof attrValue.type === 'string') { + value = attrValue.type + ' />'; + } else { + value = attrValue.type?.name ? attrValue.type.name + ' />' : helper.getElementTag(attrValue) + ' />'; + } + showType = 'string'; + const arrayItem: IAttr = { + name: index, + type: showType, + value, + indentation: parentIndentation + 2, + }; + result.push(arrayItem); + } else { + parseSubAttr(attrValue, parentIndentation + 2, String(index), result); + } + }); + }; + } else if (attr instanceof Element) { + showType = 'dom'; + value = '<' + attr.tagName.toLowerCase() + ' />'; + } else { + if (isJSXElement(attr)) { + if (typeof attr.type === 'string') { + value = attr.type + ' />'; + } else { + value = attr.type?.name ? attr.type.name + ' />' : helper.getElementTag(attr) + ' />'; + } + showType = 'string'; + } else { + showType = AttrType; + value = Object.keys(attr).length === 0 ? '{}' : parseSubTitle(attr); + addSubState = () => { + // 判断是否为 Context 循环引用 + if (isCycle(attr)) { + attr = JSON.parse(JSON.stringify(attr, decycle())); + } + Object.entries(attr).map(([key, val]) => { + if (key === '_vNode') { + val = JSON.parse(JSON.stringify(val, decycle())); + } + parseSubAttr(val, parentIndentation + 2, key, result); + }); + }; + } + } + } + + const item: IAttr = { + name: attrName, + type: showType, + value, + indentation: parentIndentation + 1, + }; + if (hIndex !== undefined) { + item.hIndex = hIndex; + } + result.push(item); + if (addSubState) { + addSubState(); + } +}; + +// 将属性的值解析成固定格式, props 和类组件的 state 必须是一个对象 +export function parseAttr(rootAttr: any) { + const result: IAttr[] = []; + const indentation = 0; + if (typeof rootAttr === 'object' && rootAttr !== null) { + Object.keys(rootAttr).forEach(key => { + parseSubAttr(rootAttr[key], indentation, key, result); + }); + } + return result; +} + +export function parseHooks( + hooks: Hook[] | null, + depContexts: Array> | null, + getHookInfo +) { + const result: IAttr[] = []; + const indentation = 0; + if (depContexts !== null && depContexts?.length > 0) { + depContexts.forEach(context => { + parseSubAttr(context.value, indentation, 'Context', result); + }); + } + hooks?.forEach(hook => { + const hookInfo = getHookInfo(hook); + if (hookInfo) { + const { name, hIndex, value } = hookInfo; + parseSubAttr(value, indentation, name, result, hIndex); + } + }); + return result; +} + +export function parseVNodeAttrs(vNode: VNode, getHookInfo) { + const tag = vNode.tag; + + if (propsAndStateTag.includes(tag)) { + const { props, state, src } = vNode; + const parsedProps = parseAttr(props); + const parsedState = parseAttr(state); + return { + parsedProps, + parsedState, + src, + }; + } else if (propsAndHooksTag.includes(tag)) { + const { props, hooks, depContexts, src } = vNode; + const parsedProps = parseAttr(props); + const parsedHooks = parseHooks(hooks, depContexts, getHookInfo); + return { + parsedProps, + parsedHooks, + src, + }; + } else if (propsTag.includes(tag)) { + const { props, src } = vNode; + const parsedProps = parseAttr(props); + return { + parsedProps, + src, + }; + } +} + +// 计算属性的访问顺序 +function calculateAttrAccessPath(item: IAttr, index: number, attrs: IAttr[], isHook: boolean) { + let currentIndentation = item.indentation; + const path: (string | number | undefined)[] = [item.name]; + let hookRootItem: IAttr = item; + for (let i = index - 1; i >= 0; i--) { + const lastItem = attrs[i]; + const lastIndentation = lastItem.indentation; + if (lastIndentation < currentIndentation) { + hookRootItem = lastItem; + path.push(lastItem.name); + currentIndentation = lastIndentation; + } + } + path.reverse(); + if (isHook) { + if (hookRootItem) { + path[0] = hookRootItem.hIndex; + } else { + console.error('There is a bug, please report'); + } + } + return path; +} + +export function buildAttrModifyData( + parsedAttrsType: string, + attrs: IAttr[], + value, + item: IAttr, + index: number, + id: number +) { + let type; + if (parsedAttrsType === 'parsedProps') { + type = ModifyProps; + } else if (parsedAttrsType === 'parsedState') { + type = ModifyState; + } else if (parsedAttrsType === 'parsedHooks') { + type = ModifyHooks; + } else { + return null; + } + + const path = calculateAttrAccessPath(item, index, attrs, parsedAttrsType === 'parsedHooks'); + + return { + id: id, + type: type, + value: value, + path: path, + }; +} From 42e8bd11715a8c745b645dcefd59fc69aecdbffa Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Thu, 9 Nov 2023 10:34:51 +0800 Subject: [PATCH 048/108] =?UTF-8?q?[inula-dev-tools]=20=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=20VNode=20tree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-dev-tools/src/parser/parseAttr.ts | 2 - .../inula-dev-tools/src/parser/parseVNode.ts | 229 ++++++++++++++++++ 2 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 packages/inula-dev-tools/src/parser/parseVNode.ts diff --git a/packages/inula-dev-tools/src/parser/parseAttr.ts b/packages/inula-dev-tools/src/parser/parseAttr.ts index 94473576..59b47073 100644 --- a/packages/inula-dev-tools/src/parser/parseAttr.ts +++ b/packages/inula-dev-tools/src/parser/parseAttr.ts @@ -28,8 +28,6 @@ import { import { helper } from '../injector'; import { JSXElement, ContextType } from '../../../inula/src/renderer/Types'; import { decycle } from 'json-decycle'; -import {arrify} from "ts-loader/dist/utils"; -import {add} from "../../../inula/src/renderer/taskExecutor/TaskQueue"; // 展示值为 string 的可编辑模型 type EditableStringType = 'string' | 'number' | 'undefined' | 'null'; diff --git a/packages/inula-dev-tools/src/parser/parseVNode.ts b/packages/inula-dev-tools/src/parser/parseVNode.ts new file mode 100644 index 00000000..0b01799f --- /dev/null +++ b/packages/inula-dev-tools/src/parser/parseVNode.ts @@ -0,0 +1,229 @@ +/* + * 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 { VNode } from '../../../inula/src/renderer/vnode/VNode'; +import { + ClassComponent, + ContextConsumer, + ContextProvider, + ForwardRef, + FunctionComponent, + MemoComponent, + SuspenseComponent +} from '../../../inula/src/renderer/vnode/VNodeTags'; + +export type NameObj = { + itemName: string; + badge: Array; +}; + +// 建立双向映射关系,当用户在修改属性值后,可以找到对应的 VNode +export let VNodeToIdMap: Map; +export let IdToVNodeMap: Map; + +if (!VNodeToIdMap) { + VNodeToIdMap = new Map(); +} + +if (!IdToVNodeMap) { + IdToVNodeMap = new Map(); +} + +let uid = 0; +function generateUid(vNode: VNode) { + const id = VNodeToIdMap.get(vNode); + if (id !== undefined) { + return id; + } + uid++; + return uid; +} + +const componentType = [ + ClassComponent, + FunctionComponent, + ContextProvider, + ContextConsumer, + ForwardRef, + SuspenseComponent, + MemoComponent, +]; + +const badgeNameArr: Array = [ + 'withRouter(', + 'SideEffect(', + 'Connect(', + 'injectIntl(', + 'Pure(', +]; + +export function isUserComponent(tag: string) { + return componentType.includes(tag); +} + +function getParentUserComponent(node: VNode) { + let parent = node.parent; + while (parent) { + if (isUserComponent(parent.tag)) { + break; + } + parent = parent.parent; + } + return parent; +} + +function getContextName(node: VNode, type: string) { + const contextType = type; + if (!node.type.displayName) { + if (node.type.value) { + if (typeof node.type.value === 'object') { + return `Context.${contextType}`; + } else { + return `${node.type.value}.${contextType}`; + } + } else { + if (node.type._context?.displayName) { + return `${node.type._context.displayName}.${contextType}`; + } + return `Context.${contextType}`; + } + } + return `${node.type.displayName}.${contextType}`; +} + +const getForwardRefName = (node: VNode): NameObj => { + const forwardRefName: NameObj = { + itemName: '', + badge: ['ForwardRef'], + }; + if (!node.type.render?.name) { + if (node.type.render?.name !== '') { + forwardRefName.itemName = node.type?.displayName ? node.type?.displayName : 'Anonymous'; + } else { + forwardRefName.itemName = 'Anonymous'; + } + } else { + forwardRefName.itemName = node.type.render?.name; + } + return forwardRefName; +}; + +// 用于结构组件名,例如: Pure(Memo(xxx)) => xxx 并且把 Pure Memo 加到 NameObj.badge 里 +const parseComponentName = (name: NameObj): NameObj => { + badgeNameArr.forEach(badgeName => { + if (name.itemName.startsWith(badgeName)) { + // 截断开头的高阶组件名,并把最后一个 ) 替换为 ''。例如: Pure(Memo(xxx)) => Memo(xxx)) => Memo(xxx) + name.itemName = name.itemName.substring(badgeName.length).replace(/(\))(?!.*\1)/, ''); + name.badge.push(badgeName.substring(0, badgeName.length - 1)); + } + }); + return name; +}; + +// 取字符串括号里的值 +const getValuesInParentheses = (name: string) => { + let result = name; + const regex = /\((.+?)\)/g; + const results = name.match(regex); + if (results) { + const option = results[0]; + if (option) { + result = option.substring(1, option.length - 1); + } + } + return result; +}; + +function isNullOrUndefined(prop) { + return !prop || typeof prop === 'undefined' || prop === 0; +} + +function parseTreeRoot(travelVNodeTree, treeRoot: VNode) { + const result: any[] = []; + travelVNodeTree(treeRoot, (node: VNode) => { + const tag = node.tag; + + if (isUserComponent(tag)) { + // 添加 ID + const id = generateUid(node); + result.push(id); + let nameObj: NameObj = { + itemName: '', + badge: [], + }; + // 拿到不同类型的展示名字 + if (tag === ContextProvider) { + nameObj.itemName = getContextName(node, 'Provider'); + result.push(nameObj); + } else if (tag === ContextConsumer) { + nameObj.itemName = getContextName(node, 'Consumer'); + result.push(nameObj); + } else if (tag === ForwardRef) { + const name = getForwardRefName(node); + result.push(name); + } else if (tag === SuspenseComponent) { + nameObj.itemName = 'Suspense'; + result.push(nameObj); + } else if (tag === MemoComponent) { + const name = node.type?.displayName || node.type?.name || node.type.render?.name; + nameObj.itemName = !isNullOrUndefined(name) ? name : 'Anonymous'; + nameObj.badge.push('Memo'); + nameObj = parseComponentName(nameObj); + result.push(nameObj); + } else { + const name = node.type.displayName || node.type?.name; + nameObj.itemName = !isNullOrUndefined(name) ? name : 'Anonymous'; + nameObj = parseComponentName(nameObj); + result.push(nameObj); + } + + // 添加父节点 ID + const parent = getParentUserComponent(node); + if (parent) { + const parentId = VNodeToIdMap.get(parent); + result.push(parentId); + } else { + result.push(''); + } + + // 添加节点 key 值 + const key = node.key; + if (key !== null) { + result.push(key); + } else { + result.push(''); + } + + VNodeToIdMap.set(node, id); + IdToVNodeMap.set(id, node); + } + }); + + return result; +} + +export function queryVNode(id: number): VNode | undefined { + return IdToVNodeMap.get(id); +} + +export function clearVNode(vNode: VNode) { + if (VNodeToIdMap.has(vNode)) { + const id = VNodeToIdMap.get(vNode); + VNodeToIdMap.delete(vNode); + IdToVNodeMap.delete(id); + } +} + +export default parseTreeRoot; From 8f33376722b2ddabbb2784aadf471966d1147230 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Thu, 9 Nov 2023 15:37:51 +0800 Subject: [PATCH 049/108] =?UTF-8?q?[inula-dev-tools]=20=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E6=A0=91=E9=80=89=E4=B8=AD=E9=AB=98=E4=BA=AE=E5=90=88?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-dev-tools/src/highlight/index.ts | 274 ++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 packages/inula-dev-tools/src/highlight/index.ts diff --git a/packages/inula-dev-tools/src/highlight/index.ts b/packages/inula-dev-tools/src/highlight/index.ts new file mode 100644 index 00000000..78ae260b --- /dev/null +++ b/packages/inula-dev-tools/src/highlight/index.ts @@ -0,0 +1,274 @@ +/* + * 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 assign from 'object-assign'; +import { VNode } from '../../../inula/src/renderer/vnode/VNode'; + +const overlayStyles = { + background: 'rgba(120, 170, 210, 0.7)', + padding: 'rgba(77, 200, 0, 0.3)', + margin: 'rgba(255, 155, 0, 0.3)', + border: 'rgba(255, 200, 50, 0.3)' +}; + +type Rect = { + bottom: number; + height: number; + left: number; + right: number; + top: number; + width: number; +}; + +function setBoxStyle(eleStyle, boxArea, node) { + assign(node.style, { + borderTopWidth: eleStyle[boxArea + 'Top'] + 'px', + borderLeftWidth: eleStyle[boxArea + 'Left'] + 'px', + borderRightWidth: eleStyle[boxArea + 'Right'] + 'px', + borderBottomWidth: eleStyle[boxArea + 'Bottom'] + 'px', + }); +} + +function getOwnerWindow(node: Element): typeof window | null { + if (!node.ownerDocument) { + return null; + } + return node.ownerDocument.defaultView; +} + +function getOwnerIframe(node: Element): Element | null { + const nodeWindow = getOwnerWindow(node); + if (nodeWindow) { + return nodeWindow.frameElement; + } + return null; +} + +function getElementStyle(domElement: Element) { + const style = window.getComputedStyle(domElement); + return{ + marginLeft: parseInt(style.marginLeft, 10), + marginRight: parseInt(style.marginRight, 10), + marginTop: parseInt(style.marginTop, 10), + marginBottom: parseInt(style.marginBottom, 10), + borderLeft: parseInt(style.borderLeftWidth, 10), + borderRight: parseInt(style.borderRightWidth, 10), + borderTop: parseInt(style.borderTopWidth, 10), + borderBottom: parseInt(style.borderBottomWidth, 10), + paddingLeft: parseInt(style.paddingLeft, 10), + paddingRight: parseInt(style.paddingRight, 10), + paddingTop: parseInt(style.paddingTop, 10), + paddingBottom: parseInt(style.paddingBottom, 10) + }; +} + +function mergeRectOffsets(rects: Array): Rect { + return rects.reduce((previousRect, rect) => { + if (previousRect == null) { + return rect; + } + + return { + top: previousRect.top + rect.top, + left: previousRect.left + rect.left, + width: previousRect.width + rect.width, + height: previousRect.height + rect.height, + bottom: previousRect.bottom + rect.bottom, + right: previousRect.right + rect.right + }; + }); +} + +function getBoundingClientRectWithBorderOffset(node: Element) { + const dimensions = getElementStyle(node); + return mergeRectOffsets([ + node.getBoundingClientRect(), + { + top: dimensions.borderTop, + left: dimensions.borderLeft, + bottom: dimensions.borderBottom, + right:dimensions.borderRight, + // 高度和宽度不会被使用 + width: 0, + height: 0 + } + ]); +} + +function getNestedBoundingClientRect( + node: HTMLElement, + boundaryWindow +): Rect { + const ownerIframe = getOwnerIframe(node); + if (ownerIframe && ownerIframe !== boundaryWindow) { + const rects = [node.getBoundingClientRect()] as Rect[]; + let currentIframe = ownerIframe; + let onlyOneMore = false; + while (currentIframe) { + const rect = getBoundingClientRectWithBorderOffset(currentIframe); + rects.push(rect); + currentIframe = getOwnerIframe(currentIframe); + + if (onlyOneMore) { + break; + } + + if (currentIframe &&getOwnerWindow(currentIframe) === boundaryWindow) { + onlyOneMore = true; + } + } + + return mergeRectOffsets(rects); + } else { + return node.getBoundingClientRect(); + } +} + +// 用来遮罩 +class OverlayRect { + node: HTMLElement; + border: HTMLElement; + padding: HTMLElement; + content: HTMLElement; + + constructor(doc: Document, container: HTMLElement) { + this.node = doc.createElement('div'); + this.border = doc.createElement('div'); + this.padding = doc.createElement('div'); + this.content = doc.createElement('div'); + + this.border.style.borderColor = overlayStyles.border; + this.padding.style.borderColor = overlayStyles.padding; + this.content.style.backgroundColor = overlayStyles.background; + + assign(this.node.style, { + borderColor: overlayStyles.margin, + pointerEvents: 'none', + position: 'fixed' + }); + + this.node.style.zIndex = '10000000'; + + this.node.appendChild(this.border); + this.border.appendChild(this.padding); + this.padding.appendChild(this.content); + container.appendChild(this.node); + } + + remove() { + if (this.node.parentNode) { + this.node.parentNode.removeChild(this.node); + } + } + + update(boxRect: Rect, eleStyle: any) { + setBoxStyle(eleStyle, 'margin', this.node); + setBoxStyle(eleStyle, 'border', this.border); + setBoxStyle(eleStyle, 'padding', this.padding); + + assign(this.content.style, { + height: boxRect.height - eleStyle.borderTop - eleStyle.borderBottom - eleStyle.paddingTop - eleStyle.paddingBottom + 'px', + width: boxRect.width - eleStyle.borderLeft - eleStyle.borderRight - eleStyle.paddingLeft - eleStyle.paddingRight + 'px' + }); + + assign(this.node.style, { + top: boxRect.top - eleStyle.marginTop + 'px', + left: boxRect.left - eleStyle.marginLeft + 'px' + }); + } +} + +class ElementOverlay { + window: typeof window; + container: HTMLElement; + rects: Array; + + constructor() { + this.window = window; + const doc = window.document; + this.container = doc.createElement('div'); + this.container.style.zIndex = '10000000'; + this.rects = []; + + doc.body.appendChild(this.container); + } + + remove() { + this.rects.forEach(rect => { + rect.remove(); + }); + this.rects.length = 0; + if (this.container.parentNode) { + this.container.parentNode.removeChild(this.container); + } + } + + execute(nodes: Array) { + const elements = nodes.filter(node => node.tag === 'DomComponent'); + + // 有几个 element 就添加几个 OverlayRect + while (this.rects.length > elements.length) { + const rect = this.rects.pop(); + rect.remove(); + } + if (elements.length === 0) { + return; + } + + while (this.rects.length < elements.length) { + this.rects.push(new OverlayRect(this.window.document, this.container)); + } + + const outerBox = { + top: Number.POSITIVE_INFINITY, + right: Number.NEGATIVE_INFINITY, + bottom: Number.NEGATIVE_INFINITY, + left: Number.POSITIVE_INFINITY + }; + + elements.forEach((element, index) => { + const eleStyle = getElementStyle(element.realNode); + const boxRect = getNestedBoundingClientRect(element.realNode, this.window); + + outerBox.top = Math.min(outerBox.top, boxRect.top - eleStyle.marginTop); + outerBox.right = Math.max(outerBox.right, boxRect.left + boxRect.width + eleStyle.marginRight); + outerBox.bottom = Math.max(outerBox.bottom, boxRect.top + boxRect.height + eleStyle.marginBottom); + outerBox.left = Math.min(outerBox.left, boxRect.left - eleStyle.marginLeft); + + const rect = this.rects[index]; + rect.update(boxRect, eleStyle); + }); + } +} + +let elementOverlay: ElementOverlay | null = null; +export function hideHighlight() { + if (elementOverlay !== null) { + elementOverlay.remove(); + elementOverlay = null; + } +} + +export function showHighlight(elements: Array | null) { + if (window.document == null || elements == null) { + return; + } + + if (elementOverlay === null) { + elementOverlay = new ElementOverlay(); + } + + elementOverlay.execute(elements); +} From 559a6fc3aaddd2cdbb9bee330760970004abe3f9 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Mon, 13 Nov 2023 16:52:51 +0800 Subject: [PATCH 050/108] =?UTF-8?q?[inula-dev-tools]=20injector=20?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=90=88=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-dev-tools/src/injector/index.ts | 485 ++++++++++++++++++ .../src/injector/pickElement.ts | 96 ++++ 2 files changed, 581 insertions(+) create mode 100644 packages/inula-dev-tools/src/injector/index.ts create mode 100644 packages/inula-dev-tools/src/injector/pickElement.ts diff --git a/packages/inula-dev-tools/src/injector/index.ts b/packages/inula-dev-tools/src/injector/index.ts new file mode 100644 index 00000000..3cf3cf2d --- /dev/null +++ b/packages/inula-dev-tools/src/injector/index.ts @@ -0,0 +1,485 @@ +/* + * 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 parseTreeRoot, { clearVNode, queryVNode, VNodeToIdMap } from '../parser/parseVNode'; +import { packagePayload, checkMessage } from '../utils/transferUtils'; +import { + RequestAllVNodeTreeInfos, + AllVNodeTreeInfos, + RequestComponentAttrs, + ComponentAttrs, + DevToolHook, + DevToolContentScript, + ModifyAttrs, + ModifyHooks, + ModifyState, + ModifyProps, + InspectDom, + LogComponentData, + Highlight, + RemoveHighlight, + ViewSource, + PickElement, + StopPickElement, + CopyToConsole, + StorageValue, +} from '../utils/constants'; +import { VNode } from '../../../inula/src/renderer/vnode/VNode'; +import { parseVNodeAttrs } from '../parser/parseAttr'; +import { showHighlight, hideHighlight } from '../highlight'; +import { + FunctionComponent, + ClassComponent, + IncompleteClassComponent, + ForwardRef, + MemoComponent +} from '../../../inula/src/renderer/vnode/VNodeTags'; +import { pickElement } from './pickElement'; + +const roots = []; +let storeDataCount = 0; + +function addIfNotInclude(treeRoot: VNode) { + if (!roots.includes(treeRoot)) { + roots.push(treeRoot); + } +} + +function send() { + const result = roots.reduce((pre, current) => { + const info = parseTreeRoot(helper.travelVNodeTree, current); + pre.push(info); + return pre; + }, []); + postMessage(AllVNodeTreeInfos, result); +} + +function deleteVNode(vNode: VNode) { + // 开发工具中保存了 vNode 的引用,在清理 vNode 的时候需要一并删除 + clearVNode(vNode); + const index = roots.indexOf(vNode); + if (index !== -1) { + roots.splice(index, 1); + } +} + +export function postMessage(type: string, data) { + window.postMessage( + packagePayload( + { + type: type, + data: data, + }, + DevToolHook + ), + '*' + ); +} + +function parseCompAttrs(id: number) { + const vNode = queryVNode(id); + if (!vNode) { + console.error('Do not find match vNode, this is a bug, please report us.'); + return; + } + const parsedAttrs = parseVNodeAttrs(vNode, helper.getHookInfo); + postMessage(ComponentAttrs, parsedAttrs); +} + +function calculateNextValue(editValue, value, attrPath) { + let nextState; + const editValueType = typeof editValue; + if ( + editValueType === 'string' || + editValueType === 'undefined' || + editValueType === 'boolean' + ) { + nextState = value; + } else if (editValueType === 'number') { + const numValue = Number(value); + nextState = isNaN(numValue) ? value : numValue; // 如果能转为数字,转数字,不能转数字旧用原值 + } else if (editValueType === 'object') { + if (editValue === null) { + nextState = value; + } else { + const newValue = Array.isArray(editValue) ? [...editValue] : { ...editValue }; + // 遍历读取到直接指向需要修改值的对象 + let attr = newValue; + for (let i = 0; i < attrPath.length - 1; i++) { + attr = attr[attrPath[i]]; + } + // 修改对象上的值 + attr[attrPath[attrPath.length - 1]] = value; + nextState = newValue; + } + } else { + console.error( + 'The dev tools tried to edit a non-editable value, this is a bug, please report.', + editValue + ); + } + return nextState; +} + +function modifyVNodeAttrs(data) { + const { type, id, value, path } = data; + const vNode = queryVNode(id); + if (!vNode) { + console.error('Do not find match vNode, this is a bug, please report us.'); + return; + } + if (type === ModifyProps) { + const nextProps = calculateNextValue(vNode.props, value, path); + helper.updateProps(vNode, nextProps); + } else if (type === ModifyHooks) { + const hooks = vNode.hooks; + const editHook = hooks[path[0]]; + const hookInfo = helper.getHookInfo(editHook); + if (hookInfo) { + const editValue = hookInfo.value; + // path 的第一个指向 hIndex,从第二个值才开始指向具体属性访问路径 + const nextState = calculateNextValue(editValue, value, path.slice(1)); + helper.updateHooks(vNode, path[0], nextState); + } else { + console.error( + 'The dev tools tried to edit a non-editable hook, this is a bug, please report.', + hooks + ); + } + } else if (type === ModifyState) { + const oldState = vNode.state || {}; + const nextState = { ...oldState }; + let accessRef = nextState; + for (let i = 0; i < path.length - 1; i++) { + accessRef = accessRef[path[i]]; + } + accessRef[path[path.length - 1]] = value; + helper.updateState(vNode, nextState); + } +} + +function logComponentData(id: number) { + const vNode = queryVNode(id); + if (vNode == null) { + console.warn(`Could not find vNode with id "${id}"`); + return null; + } + if (vNode) { + const info = helper.getComponentInfo(vNode); + console.log('vNode: ', vNode); + console.log('Component Info: ', info); + } +} + +/** + * 通过 path 在 vNode 拿到对应的值 + * + * @param {VNode} vNode dom 节点 + * @param {Array} path 路径 + * @param {string} attrsName 值的类型(props 或者 hooks) + */ +const getValueByPath = ( + vNode: VNode, + path: Array, + attrsName: string +) => { + if (attrsName === 'Props') { + return path.reduce((previousValue, currentValue) => { + return previousValue[currentValue]; + }, vNode.props); + } else { + // attrsName 为 Hooks + if (path.length > 1) { + return path.reduce((previousValue, currentValue) => { + return previousValue[currentValue]; + }, vNode.hooks); + } + return vNode.hooks[path[0]]; + } +}; + +/** + * 通过 path 在 vNode 拿到对应的值,并且在控制台打印出来 + * + * @param {number} id idToVNodeMap 的 key 值,通过 id 拿到 VNode + * @param {string} itemName 打印出来值的名称 + * @param {Array} path 值的路径 + * @param {string} attrsName 值的类型 + */ +function logDataWithPath( + id: number, + itemName: string, + path: Array, + attrsName: string +) { + const vNode = queryVNode(id); + if (vNode === null) { + console.warn(`Could not find vNode with id "${id}"`); + return null; + } + if (vNode) { + const value = getValueByPath(vNode, path, attrsName); + if (attrsName === 'Hooks') { + console.log(itemName, value); + } else { + console.log(`${path[path.length - 1]}`, value); + } + } +} + +/** + * 通过 path 在 vNode 拿到对应的值,并且存为全局变量 + * + * @param {number} id idToVNodeMap 的 key 值,通过 id 拿到 VNode + * @param {Array} path 值的路径 + * @param {string} attrsName 值的类型 + */ +function storeDataWithPath( + id: number, + path: Array, + attrsName: string +) { + const vNode = queryVNode(id); + if (vNode === null) { + console.warn(`Could not find vNode with id "${id}"`); + return null; + } + if (vNode) { + const value = getValueByPath(vNode, path, attrsName); + const key = `$InulaTemp${storeDataCount++}`; + + window[key] = value; + console.log(key); + console.log(value); + } +} + +export let helper; + +function init(inulaHelper) { + helper = inulaHelper; + (window as any).__INULA_DEV_HOOK__.isInit = true; +} + +export function getElement(travelVNodeTree, treeRoot: VNode) { + const result: any[] = []; + travelVNodeTree( + treeRoot, + (node: VNode) => { + if (node.realNode) { + if (Object.keys(node.realNode).length > 0 || node.realNode.size > 0) { + result.push(node); + } + } + }, + (node: VNode) => + node.realNode != null && + (Object.keys(node.realNode).length > 0 || node.realNode.size > 0) + ); + return result; +} + +// dev tools 点击眼睛图标功能 +const inspectDom = data => { + const { id } = data; + const vNode = queryVNode(id); + if (vNode == null) { + console.warn(`Could not find vNode with id "${id}"`); + return null; + } + const info = getElement(helper.travelVNodeTree, vNode); + if (info) { + showHighlight(info); + (window as any).__INULA_DEV_HOOK__.$0 = info[0]; + } +}; + +const picker = pickElement(window); + +const actions = new Map([ + // 请求左树所有数据 + [ + RequestAllVNodeTreeInfos, + () => { + send(); + }, + ], + // 请求某个节点的 props,hooks + [ + RequestComponentAttrs, + data => { + parseCompAttrs(data); + }, + ], + // 修改 props,hooks + [ + ModifyAttrs, + data => { + modifyVNodeAttrs(data); + }, + ], + // 找到节点对应 element + [ + InspectDom, + data => { + inspectDom(data); + }, + ], + // 打印节点数据 + [ + LogComponentData, + data => { + logComponentData(data); + }, + ], + // 高亮 + [ + Highlight, + data => { + const node = queryVNode(data.id); + if (node == null) { + console.warn(`Could not find vNode with id "${data.id}"`); + return null; + } + const info = getElement(helper.travelVNodeTree, node); + showHighlight(info); + }, + ], + // 移出高亮 + [ + RemoveHighlight, + () => { + hideHighlight(); + }, + ], + // 查看节点源代码位置 + [ + ViewSource, + data => { + const node = queryVNode(data.id); + if (node == null) { + console.warn(`Could not find vNode with id "${data.id}"`); + return null; + } + showSource(node); + }, + ], + // 选中页面元素对应 dev tools 节点 + [ + PickElement, + () => { + picker.startPick(); + }, + ], + [ + StopPickElement, + () => { + picker.stopPick(); + }, + ], + // 在控制台打印 Props Hooks State 值 + [ + CopyToConsole, + data => { + const node = queryVNode(data.id); + if (node == null) { + console.warn(`Could not find vNode with id "${data.id}"`); + return null; + } + logDataWithPath(data.id, data.itemName, data.path, data.attrsName); + }, + ], + // 把 Props Hooks State 值存为全局变量 + [ + StorageValue, + data => { + const node = queryVNode(data.id); + if (node == null) { + console.warn(`Could not find vNode with id "${data.id}"`); + return null; + } + storeDataWithPath(data.id, data.path, data.attrsName); + }, + ], +]); + +const showSource = (node: VNode) => { + switch (node.tag) { + case ClassComponent: + case IncompleteClassComponent: + case FunctionComponent: + global.$type = node.type; + break; + case ForwardRef: + global.$type = node.type.render; + break; + case MemoComponent: + global.$type = node.type.type; + break; + default: + global.$type = null; + break; + } +}; + +const handleRequest = (type: string, data) => { + const action = actions.get(type); + if (action) { + action.call(this, data); + return null; + } + console.warn('unknown command', type); +}; + +function injectHook() { + if ((window as any).__INULA_DEV_HOOK__) { + return; + } + Object.defineProperty(window, '__INULA_DEV_HOOK__', { + enumerable: false, + value: { + $0: null, + init, + isInit: false, + addIfNotInclude, + send, + deleteVNode, + // inulaX 使用 + getVNodeId: vNode => { + return VNodeToIdMap.get(vNode); + }, + }, + }); + + window.addEventListener('message', function (event) { + // 只接收我们自己的消息 + if (event.source !== window) { + return; + } + const request = event.data; + if (checkMessage(request, DevToolContentScript)) { + const { payload } = request; + const { type, data } = payload; + + // 忽略 inulaX 的 actions + if (type.startsWith('inulax')) { + return; + } + handleRequest(type, data); + } + }); +} + +injectHook(); diff --git a/packages/inula-dev-tools/src/injector/pickElement.ts b/packages/inula-dev-tools/src/injector/pickElement.ts new file mode 100644 index 00000000..b1116faf --- /dev/null +++ b/packages/inula-dev-tools/src/injector/pickElement.ts @@ -0,0 +1,96 @@ +/* + * 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 { PickElement, StopPickElement } from '../utils/constants'; +import { getElement, helper, postMessage } from './index'; +import { queryVNode, VNodeToIdMap } from '../parser/parseVNode'; +import { isUserComponent } from '../parser/parseVNode'; +import { throttle } from 'lodash'; +import { hideHighlight, showHighlight } from '../highlight'; + +// 判断鼠标移入节点是否为 dev tools 上的节点,如果不是则找父节点 +function getUserComponent(target) { + if (target.tag && isUserComponent(target.tag)) { + return target; + } + while (target.tag && !isUserComponent(target.tag)) { + if (target.parent) { + target = target.parent; + } + } + return target; +} + +function onMouseEvent(event: MouseEvent) { + event.preventDefault(); + event.stopPropagation(); +} + +function onMouseMove(event: MouseEvent) { + event.preventDefault(); + event.stopPropagation(); + + const target = (event.target as any)._inula_VNode; + if (target) { + const id = VNodeToIdMap.get(getUserComponent(target)); + const vNode = queryVNode(id); + if (vNode == null) { + console.warn(`Could not find vNode with id "${id}"`); + return null; + } + const info = getElement(helper.travelVNodeTree, vNode); + if (info) { + showHighlight(info); + } + + // 0.5 秒内在节流结束后只触发一次 + throttle( + () => { + postMessage(PickElement, id); + }, + 500, + { leading: false, trailing: true } + )(); + } +} + +export function pickElement(window: Window) { + function onClick(event: MouseEvent) { + event.preventDefault(); + event.stopPropagation(); + + stopPick(); + postMessage(StopPickElement, null); + } + + const startPick = () => { + if (window && typeof window.addEventListener === 'function') { + window.addEventListener('click', onClick, true); + window.addEventListener('mousedown', onMouseEvent, true); + window.addEventListener('mousemove', onMouseMove, true); + window.addEventListener('mouseup', onMouseEvent, true); + } + }; + + const stopPick = () => { + hideHighlight(); + window.removeEventListener('click', onClick, true); + window.removeEventListener('mousedown', onMouseEvent, true); + window.removeEventListener('mousemove', onMouseMove, true); + window.removeEventListener('mouseup', onMouseEvent, true); + }; + + return { startPick, stopPick }; +} From ef39cc590902f868e10887048592c3929b51ab4a Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Mon, 13 Nov 2023 17:12:15 +0800 Subject: [PATCH 051/108] =?UTF-8?q?[inula-dev-tools]=20=E4=B8=8E=20p?= =?UTF-8?q?anel=20=E5=BB=BA=E7=AB=8B=E8=BF=9E=E6=8E=A5=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/panelConnection/index.ts | 16 ++++ .../src/panelConnection/panelConnection.ts | 78 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 packages/inula-dev-tools/src/panelConnection/index.ts create mode 100644 packages/inula-dev-tools/src/panelConnection/panelConnection.ts diff --git a/packages/inula-dev-tools/src/panelConnection/index.ts b/packages/inula-dev-tools/src/panelConnection/index.ts new file mode 100644 index 00000000..96234936 --- /dev/null +++ b/packages/inula-dev-tools/src/panelConnection/index.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ + +export * from './panelConnection'; diff --git a/packages/inula-dev-tools/src/panelConnection/panelConnection.ts b/packages/inula-dev-tools/src/panelConnection/panelConnection.ts new file mode 100644 index 00000000..56941206 --- /dev/null +++ b/packages/inula-dev-tools/src/panelConnection/panelConnection.ts @@ -0,0 +1,78 @@ +/* + * 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 { packagePayload } from '../utils/transferUtils'; +import { DevToolPanel, InitDevToolPageConnection } from '../utils/constants'; + +let connection; +const callbacks = []; + +export function addBackgroundMessageListener(func: (message) => void) { + callbacks.push(func); +} + +export function removeBackgroundMessageListener(func: (message) => void) { + const index = callbacks.indexOf(func); + if (index !== -1) { + callbacks.splice(index, 1); + } +} + +export function initBackgroundConnection(type) { + if (!isDev) { + try { + connection = chrome.runtime.connect({ name: type }); + const notice = message => { + callbacks.forEach(func => { + func(message); + }); + }; + // 监听 background 消息 + connection.onMessage.addListener(notice); + // 页面打开后发送初始化请求 + postMessageToBackground(InitDevToolPageConnection); + } catch (e) { + console.error('create connection failer'); + console.error(e); + } + } +} + +let reconnectionTimes = 0; +export function postMessageToBackground( + type: string, + data?: any, + inulaX?: boolean +) { + try { + const payload = data + ? { type, tabId: chrome.devtools.inspectedWindow.tabId, data } + : { type, tabId: chrome.devtools.inspectedWindow.tabId }; + connection.postMessage(packagePayload(payload, DevToolPanel)); + } catch (e) { + // 可能出现 port 关闭的场景,需要重新建立连接,增加可靠性 + if (reconnectionTimes === 20) { + reconnectionTimes = 0; + console.error('reconnect failed'); + return; + } + console.error(e); + reconnectionTimes++; + // 重新连接 + initBackgroundConnection(inulaX ? 'panelX' : 'panel'); + // 初始化成功后才会重新发送消息 + postMessageToBackground(type, data); + } +} From 2efd4f8f37e984b3291602b0212d5fdaae626187 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Mon, 13 Nov 2023 20:19:37 +0800 Subject: [PATCH 052/108] =?UTF-8?q?[inula-dev-tools]=20=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E6=A0=91=E7=BB=84=E4=BB=B6=E5=90=88=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-dev-tools/src/components/VTree.less | 78 +++++ .../inula-dev-tools/src/components/VTree.tsx | 329 ++++++++++++++++++ .../src/components/assets.less | 32 ++ packages/inula-dev-tools/tsconfig.json | 3 +- 4 files changed, 440 insertions(+), 2 deletions(-) create mode 100644 packages/inula-dev-tools/src/components/VTree.less create mode 100644 packages/inula-dev-tools/src/components/VTree.tsx create mode 100644 packages/inula-dev-tools/src/components/assets.less diff --git a/packages/inula-dev-tools/src/components/VTree.less b/packages/inula-dev-tools/src/components/VTree.less new file mode 100644 index 00000000..54d23bb7 --- /dev/null +++ b/packages/inula-dev-tools/src/components/VTree.less @@ -0,0 +1,78 @@ +/* + * 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 'assets.less'; + +.treeContainer { + height: 100%; + + .treeItem { + width: 100%; + position: absolute; + line-height: 1.125rem; + align-items: center; + display: inline-flex; + &:hover { + background-color: @select-color; + } + + .treeIcon { + color: @arrow-color; + display: inline-block; + width: 12px; + padding-left: 0.5rem; + } + + .componentName { + white-space: nowrap; + color: @component-name-color; + display: inline-flex; + } + + .componentKeyName { + color: @component-key-color; + } + + .componentKeyValue { + color: @component-key-value-color; + max-width: 100px; + overflow-x: hidden; + text-overflow: ellipsis; + display: inline-flex; + white-space: nowrap; + } + } + + .selectedItemChild { + background-color: @select-item-child-color; + } + + .select { + background-color: @select-color; + } +} + +.Badge { + display: inline-block; + background-color: rgba(0, 0, 0, 0.1); + color: #000000; + padding: 0 0.25rem; + line-height: normal; + border-radius: 0.125rem; + margin-left: 0.25rem; + font-family: @attr-name-font-family; + font-size: 9px; + height: 1rem; +} diff --git a/packages/inula-dev-tools/src/components/VTree.tsx b/packages/inula-dev-tools/src/components/VTree.tsx new file mode 100644 index 00000000..77918715 --- /dev/null +++ b/packages/inula-dev-tools/src/components/VTree.tsx @@ -0,0 +1,329 @@ +/* + * 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 { useState, useEffect, useCallback, memo } from 'openinula'; +import styles from './VTree.less'; +import Triangle from '../svgs/Triangle'; +import { createRegExp } from '../utils/regExpUtil'; +import { SizeObserver } from './SizeObserver'; +import { RenderInfoType, VList } from './VList'; +import { postMessageToBackground } from '../panelConnection'; +import { Highlight, RemoveHighlight } from '../utils/constants'; +import { NameObj } from '../parser/parseVNode'; + +export interface IData { + id: number; + name: NameObj; + indentation: number; + userKey: string; +} + +interface IItem { + indentationLength: number; + hasChild: boolean; + onCollapse: (data: IData) => void; + onClick: (id: IData) => void; + onMouseEnter: (id: IData) => void; + onMouseLeave: (id: IData) => void; + isCollapsed: boolean; + isSelect: boolean; + highlightValue: string; + data: IData; + isSelectedItemChild: boolean; +} + +function Item(props: IItem) { + const { + hasChild, + onCollapse, + isCollapsed, + data, + onClick, + indentationLength, + onMouseEnter, + onMouseLeave, + isSelect, + highlightValue = '', + isSelectedItemChild, + } = props; + + const { name, userKey, indentation } = data; + + const isShowKey = userKey !== ''; + const showIcon = hasChild ? : ''; + const handleClickCollapse = () => { + onCollapse(data); + }; + const handleClick = () => { + onClick(data); + }; + const handleMouseEnter = () => { + onMouseEnter(data); + }; + const handleMouseLeave = () => { + onMouseLeave(data); + }; + + const itemAttr: Record = { + className: isSelectedItemChild ? styles.selectedItemChild : styles.treeItem, + onClick: handleClick, + onMouseEnter: handleMouseEnter, + onMouseLeave: handleMouseLeave, + }; + + if (isSelect) { + itemAttr.tabIndex = 0; + itemAttr.className = styles.treeItem + ' ' + styles.select; + } + + if (isSelectedItemChild) { + itemAttr.className = styles.treeItem + ' ' + styles.selectedItemChild; + } + + const pushBadge = (showName: Array, badgeName: string) => { + showName.push(' '); + showName.push(
{badgeName}
); + }; + + const pushItemName = (showName: Array, cutName: string, char: string) => { + const index = cutName.search(char); + if (index > -1) { + const notHighlightStr = cutName.slice(0, index); + showName.push(`<${notHighlightStr}`); + showName.push({char}); + showName.push(`${cutName.slice(index + char.length)}>`); + } else { + showName.push(`<${cutName}`); + } + }; + + const pushBadgeName = (showName: Array, cutName: string, char: string) => { + const index = cutName.search(char); + if (index > -1) { + const notHighlightStr = cutName.slice(0, index); + showName.push( +
+ {notHighlightStr} + {{char}} + {cutName.slice(index + char.length)} +
+ ); + } else { + pushBadge(showName, cutName); + } + }; + + const reg = createRegExp(highlightValue); + const heightCharacters = name.itemName.match(reg); + const showName = []; + + const addShowName = (showName: Array, name: NameObj) => { + showName.push(`<${name.itemName}>`); + name.badge.forEach(key => { + showName.push(
{key}
); + }); + }; + + if (heightCharacters) { + // 高亮第一次匹配即可 + const char = heightCharacters[0]; + pushItemName(showName, name.itemName, char); + if (name.badge.length > 0) { + name.badge.forEach(key => { + pushBadgeName(showName, key, char); + }); + } + } else { + addShowName(showName, name); + } + + return ( +
+
+ {showIcon} +
+ {showName} + {isShowKey && ( + <> +  key + {'="'} + {userKey} + {'"'} + + )} +
+ ); +} + +function VTree(props: { + data: IData[]; + maxDeep: number; + highlightValue: string; + scrollToItem: IData; + onRendered: (renderInfo: RenderInfoType) => void; + collapsedNodes?: IData[]; + onCollapseNode?: (item: IData[]) => void; + selectItem: IData; + onSelectItem: (item: IData) => void; +}) { + const { + data, + maxDeep, + highlightValue, + scrollToItem, + onRendered, + onCollapseNode, + onSelectItem + } = props; + const [collapseNode, setCollapseNode] = useState(props.collapsedNodes || []); + const [selectItem, setSelectItem] = useState(props.selectItem); + const [childItems, setChildItems] = useState>([]); + + useEffect(() => { + setSelectItem(scrollToItem); + }, [scrollToItem]); + + useEffect(() => { + if (props.selectItem !== selectItem) { + setSelectItem(props.selectItem); + } + }, [props.selectItem]); + + useEffect(() => { + setCollapseNode(props.collapsedNodes || []); + }, [props.collapsedNodes]); + + const changeCollapseNode = (item: IData) => { + const nodes: IData[] = [...collapseNode]; + const index = nodes.indexOf(item); + if (index === -1) { + nodes.push(item); + } else { + nodes.splice(index, 1); + } + + setCollapseNode(nodes); + + if (onCollapseNode) { + onCollapseNode(nodes); + } + }; + + const getChildItem = (item: IData): Array => { + const index = data.indexOf(item); + const childList: Array = []; + + for (let i = index + 1; i < data.length; i++) { + if (data[i].indentation > item.indentation) { + childList.push(data[i]); + } else { + break; + } + } + return childList; + }; + + const handleClickItem = useCallback( + (item: IData) => { + const childItem = getChildItem(item); + setSelectItem(item); + setChildItems(childItem); + if (onSelectItem) { + onSelectItem(item); + } + }, + [onSelectItem] + ); + + const handleMouseEnterItem = useCallback( + item => { + postMessageToBackground(Highlight, item); + }, + null + ); + + const handleMouseLeaveItem = () => { + postMessageToBackground(RemoveHighlight); + }; + + let currentCollapseIndentation: null | number = null; + // 过滤掉折叠的 item,不展示在 VList 中 + const filter = (item: IData) => { + if (currentCollapseIndentation !== null) { + // 缩进更大,不显示 + if (item.indentation > currentCollapseIndentation) { + return false; + } else { + // 缩进小,说明完成了收起节点的子节点处理 + currentCollapseIndentation = null; + } + } + const isCollapsed = collapseNode.includes(item); + if (isCollapsed) { + // 该节点需要收起子节点 + currentCollapseIndentation = item.indentation; + } + return true; + }; + + const showList = data.filter(filter); + + return ( + + {(width: number, height: number) => { + return ( + + {(item: IData, indentationLength: number) => { + const isCollapsed = collapseNode.includes(item); + const index = showList.indexOf(item); + // 如果收起,一定有 child + // 不收起场景,如果存在下一个节点,并且节点缩进比自己大,说明下个节点是子节点,节点本身需要显示展开收起图标 + const hasChild = isCollapsed || showList[index + 1]?.indentation > item.indentation; + return ( + + ); + }} + + ); + }} + + ); +} + +export default memo(VTree); diff --git a/packages/inula-dev-tools/src/components/assets.less b/packages/inula-dev-tools/src/components/assets.less new file mode 100644 index 00000000..918c1291 --- /dev/null +++ b/packages/inula-dev-tools/src/components/assets.less @@ -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. + */ + +@arrow-color: rgb(95, 99, 104); +@divider-color: rgb(202, 205, 209); +@attr-name-color: rgb(200, 0, 0); +@component-name-color: rgb(136, 18, 128); +@component-key-color: rgb(153, 69, 0); +@component-key-value-color: rgb(26, 26, 166); +@component-attr-color: rgb(200, 0, 0); +@select-color: rgb(144 199 248 / 60%); +@select-item-child-color: rgb(141 199 248 / 40%); +@hover-color: black; + +@top-height: 2.625rem; +@divider-width: 0.2px; +@common-font-size: 12px; + +@divider-style: @divider-color solid @divider-width; +@attr-name-font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; diff --git a/packages/inula-dev-tools/tsconfig.json b/packages/inula-dev-tools/tsconfig.json index d0c62d99..d07996f8 100644 --- a/packages/inula-dev-tools/tsconfig.json +++ b/packages/inula-dev-tools/tsconfig.json @@ -2,7 +2,6 @@ "compilerOptions": { "outDir": "./dist", "allowJs": true, - "strict": true, "noImplicitAny": false, "module": "es6", "moduleResolution": "node", @@ -16,6 +15,6 @@ } }, "include": [ - "./src/**/*.ts", "./src/index.d.ts", "./src/**/*.tsx", "./externals.d.ts" + "./src/**/*.ts", "./src/index.d.ts", "./src/**/*.tsx", "./externals.d.ts", "./global.d.ts" ] } From 8c559f644dfb73da00f1005f3823ac5186713e3f Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 14 Nov 2023 16:45:17 +0800 Subject: [PATCH 053/108] [inula][create-inula] version updates --- .../lib/generators/Simple-app/templates/vite/package.json | 2 +- .../lib/generators/Simple-app/templates/webpack/package.json | 2 +- packages/create-inula/package.json | 2 +- packages/inula/package.json | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/create-inula/lib/generators/Simple-app/templates/vite/package.json b/packages/create-inula/lib/generators/Simple-app/templates/vite/package.json index 706ca751..e276cd82 100644 --- a/packages/create-inula/lib/generators/Simple-app/templates/vite/package.json +++ b/packages/create-inula/lib/generators/Simple-app/templates/vite/package.json @@ -11,7 +11,7 @@ "author": "", "license": "ISC", "dependencies": { - "openinula": "^0.0.11" + "openinula": "^0.0.1" }, "devDependencies": { "@babel/core": "^7.21.4", diff --git a/packages/create-inula/lib/generators/Simple-app/templates/webpack/package.json b/packages/create-inula/lib/generators/Simple-app/templates/webpack/package.json index c8d7813d..f2d2f0f4 100644 --- a/packages/create-inula/lib/generators/Simple-app/templates/webpack/package.json +++ b/packages/create-inula/lib/generators/Simple-app/templates/webpack/package.json @@ -10,7 +10,7 @@ "author": "", "license": "ISC", "dependencies": { - "openinula": "^0.0.11" + "openinula": "^0.0.1" }, "devDependencies": { "@babel/core": "^7.21.4", diff --git a/packages/create-inula/package.json b/packages/create-inula/package.json index 5cea2800..27f2bbee 100644 --- a/packages/create-inula/package.json +++ b/packages/create-inula/package.json @@ -1,6 +1,6 @@ { "name": "create-inula", - "version": "0.0.2", + "version": "0.0.3", "description": "", "main": "index.js", "bin": { diff --git a/packages/inula/package.json b/packages/inula/package.json index 3ec98d8b..2eb04d4b 100644 --- a/packages/inula/package.json +++ b/packages/inula/package.json @@ -4,7 +4,7 @@ "keywords": [ "openinula" ], - "version": "0.0.1", + "version": "0.0.4", "homepage": "", "bugs": "", "license": "MulanPSL2", @@ -23,6 +23,6 @@ "test": "jest --config=jest.config.js", "watch-test": "yarn test --watch --dev" }, - "files": ["build/@types", "build/cjs", "build/umd", "build/index.js", "build/jsx-dev-runtime.js", "build/jsx-runtime.js", "README.md"], + "files": ["build/**/*", "README.md"], "types": "./build/@types/index.d.ts" } From 6b59405236e616da2627da73568ce9827d2328f8 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 14 Nov 2023 16:47:04 +0800 Subject: [PATCH 054/108] =?UTF-8?q?[inula-dev-tools]=20=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E7=BB=84=E4=BB=B6=E5=90=88=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/ComponentInfo.less | 279 +++++++++++ .../src/components/ComponentInfo.tsx | 435 ++++++++++++++++++ 2 files changed, 714 insertions(+) create mode 100644 packages/inula-dev-tools/src/components/ComponentInfo.less create mode 100644 packages/inula-dev-tools/src/components/ComponentInfo.tsx diff --git a/packages/inula-dev-tools/src/components/ComponentInfo.less b/packages/inula-dev-tools/src/components/ComponentInfo.less new file mode 100644 index 00000000..d8b8f432 --- /dev/null +++ b/packages/inula-dev-tools/src/components/ComponentInfo.less @@ -0,0 +1,279 @@ +/* + * 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 'assets.less'; + +.infoContainer { + display: flex; + flex-direction: column; + height: 100%; + + .button { + border: none; + padding: 0; + border-radius: 0.25rem; + flex: 0 0 auto; + cursor: pointer; + color: #5f6673; + } + + .button :hover { + color: #23272f; + } + + .componentInfoHead { + flex: 0 0 @top-height; + display: flex; + align-items: center; + border-bottom: @divider-style; + + .name { + flex: 1 1 auto; + padding: 0 1rem 0 1rem; + .text { + display: block; + } + } + + .eye { + flex: 0 0 1rem; + cursor: pointer; + display: inline-flex; + align-items: center; + padding: 0.25rem 0.5rem 0.25rem 0.25rem; + } + + .debug { + flex: 0 0 1rem; + cursor: pointer; + display: inline-flex; + align-items: center; + padding: 0.25rem 0.5rem 0.25rem 0; + } + + .location { + flex: 0 0 1rem; + cursor: pointer; + display: inline-flex; + align-items: center; + padding: 0.25rem 0.5rem 0.25rem 0; + } + } + + .componentInfoMain { + overflow-y: auto; + + > :last-child { + border-bottom: unset; + } + + > div { + border-bottom: @divider-style; + } + + .attrContainer { + flex: 0 0; + + .attrHead { + display: flex; + flex-direction: row; + align-items: center; + padding: 0.5rem 0.5rem 0 0.5rem; + + .attrType { + flex: 1 1 0; + } + + .attrCopy { + flex: 0 0 1rem; + padding-right: 1rem; + } + } + + .attrDetail { + padding-bottom: 0.5rem; + + .attrArrow { + color: @arrow-color; + width: 12px; + display: inline-block; + } + + .attrName { + margin-top: 1px; + color: @attr-name-color; + font-family: @attr-name-font-family; + } + + .colon { + margin-top: 1px; + transform: translateY(-8%); + margin-right: 0.5rem; + } + + .info { + display: flex; + &:hover { + .operation { + visibility: visible; + .operationIcon :hover { + border: none; + border-radius: 5px; + background-color: lightskyblue; + } + } + } + } + + .attrValue { + width: 26rem; + height: 1rem; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, + Courier, monospace; + &:focus { + color: unset; + background-color: #f0f0f0; + } + } + .attrValue[data-type='string'] { + color: #009906; + } + .attrValue[data-type='function'] { + color: royalblue; + } + .attrValue[data-type='number'] { + color: #ff5722; + } + .attrValue[data-type='boolean'] { + color: #03a9f4; + } + + .operation { + cursor: pointer; + visibility: hidden; + } + + .checkBox { + margin: 2px 3px 0 auto; + justify-content: flex-end; + } + } + } + + .dropdown.active { + display: unset; + top: var(--content-top); + left: var(--content-left); + position: absolute; + ul { + margin-block-start: 0; + padding-inline-start: 0; + li { + padding: 10px; + border-top: 1px lighten(#333, 2%) solid; + height: auto; + overflow: auto; + opacity: 1; + } + } + } + + .dropdown { + display: none; + + ul { + display: block; + position: relative; + list-style: none; + } + + li { + padding: 0 10px; + background: darken(#333, 2%); + color: darken(#EEE, 40%); + text-align: left; + border: 0; + width: 100%; + height: 0; + overflow: hidden; + cursor: pointer; + opacity: 0; + transition-property: all, background-color; + transition-duration: 0.2s, 0.4s; + + &:hover, &.selected { + background-color: darken(#333, 10%); + } + + &:active { + background: #03a9f4; + } + + &:first-child { + border-radius: 5px 5px 0 0; + } + + &:last-child { + border-radius: 0 0 5px 5px; + } + + &:before { + margin-top: -2px; + margin-right: 10px; + display: inline-block; + border-radius: 5px; + vertical-align: middle; + width: 16px; + height: 16px; + } + + &:nth-child(1) { + &:before { + content: url('../svgs/copy.svg'); + } + } + + &:nth-child(2) { + &:before { + content: url('../svgs/storage.svg'); + } + } + } + } + } + + .parentsInfo { + flex: 1 1 0; + + .parentName { + padding: 0.5rem 0.5rem 0 0.5rem; + } + + .parent { + margin-left: 1.4rem; + display: block; + cursor: pointer; + text-align: left; + color: @component-name-color; + + &:hover { + background-color: @select-color; + } + } + } +} diff --git a/packages/inula-dev-tools/src/components/ComponentInfo.tsx b/packages/inula-dev-tools/src/components/ComponentInfo.tsx new file mode 100644 index 00000000..0f2b658e --- /dev/null +++ b/packages/inula-dev-tools/src/components/ComponentInfo.tsx @@ -0,0 +1,435 @@ +/* + * 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 styles from './ComponentInfo.less'; +import Eye from '../svgs/Eye'; +import Debug from '../svgs/Debug'; +import Location from '../svgs/Location'; +import Triangle from '../svgs/Triangle'; +import { memo, useContext, useEffect, useState, useRef, useMemo, createRef } from 'openinula'; +import { IData } from './VTree'; +import { buildAttrModifyData, IAttr } from '../parser/parseAttr'; +import { postMessageToBackground } from '../panelConnection'; +import { CopyToConsole, InspectDom, LogComponentData, ModifyAttrs, StorageValue } from '../utils/constants'; +import type { Source } from '../../../inula/src/renderer/Types'; +import ViewSourceContext from '../utils/ViewSource'; +import PickElementContext from '../utils/PickElement'; +import Operation from '../svgs/Operation'; + +type IComponentInfo = { + name: string; + attrs: { + parsedProps?: IAttr[]; + parsedState?: IAttr[]; + parsedHooks?: IAttr[]; + }; + parents: IData[]; + id: number; + source?: Source; + onClickParent: (item: IData) => void; +}; + +const ComponentAttr = memo(function ComponentAttr({ + attrsName, + attrsType, + attrs, + id, + dropdownRef, + }: { + attrsName: string; + attrsType: string; + attrs: IAttr[]; + id: number; + dropdownRef: null | HTMLElement; +}) { + const [editableAttrs, setEditableAttrs] = useState(attrs); + const [expandNodes, setExpandNodes] = useState([]); + + useEffect(() => { + setEditableAttrs(attrs); + }, [attrs]); + + const handleCollapse = (item: IAttr) => { + const nodes = [...expandNodes]; + const expandItem = `${item.name}_${editableAttrs.indexOf(item)}`; + const i = nodes.indexOf(expandItem); + if (i === -1) { + nodes.push(expandItem); + } else { + nodes.splice(i, 1); + } + setExpandNodes(nodes); + }; + + // props 展示的 key: value 中的 value 值 + const getShowName = item => { + let retStr; + if (item === undefined) { + retStr = String(item); + } else if (typeof item === 'number') { + retStr = item; + } else if (typeof item === 'string') { + retStr = item.endsWith('>') ? `<${item}` : item; + } else { + retStr = `"${item}"`; + } + return retStr; + }; + + /** + * 拿到 props 或 hooks 在 VNode 里的路径 + * + * @param {Array} editableAttrs 所有 props 与 hooks 的值 + * @param {number} index 此值在 editableAttrs 的下标位置 + * @param {string} attrsType 此值属于 props 还是 hooks + * @return {Array} 值在 vNode 里的路径 + */ + const getPath = (editableAttrs: IAttr[], index: number, attrsType: string): Array => { + const path: Array = []; + let local = editableAttrs[index].indentation; + if (local === 1) { + path.push(attrsType === 'Hooks' ? editableAttrs[index].hIndex : editableAttrs[index].name); + } else { + let location = local; + let id = index; + while (location > 0) { + // local === 1 时处于 vNode.hooks 的子元素最外层 + if (location < local || id === index || local === 1) { + if (local === 1) { + attrsType === 'Hooks' + ? path.unshift(editableAttrs[id + 1].hIndex, 'state') + : path.unshift(editableAttrs[id + 1].name); + break; + } else { + if (editableAttrs[id]?.indentation === 1) { + if (editableAttrs[id]?.name === 'State') { + path.unshift('stateValue'); + } + if (editableAttrs[id]?.name === 'Ref') { + path.unshift('current'); + } + } else { + path.unshift(editableAttrs[id].name); + } + } + // 跳过同级 + local = location; + } + location = id >= 1 ? editableAttrs[id - 1].indentation : -1; + id = -1; + } + } + return path; + }; + + const showAttr = []; + let currentIndentation = null; + + // 为每一行数据添加一个 ref + const refsById = useMemo(() => { + const refs = {}; + editableAttrs.forEach((item, index) => { + refs[index] = createRef(); + }); + return refs; + }, [editableAttrs]); + + editableAttrs.forEach((item, index) => { + const operationRef = refsById[index]; + const indentation = item.indentation; + if (currentIndentation !== null) { + if (indentation > currentIndentation) { + return; + } else { + currentIndentation = null; + } + } + const nextItem = editableAttrs[index + 1]; + const hasChild = nextItem ? nextItem.indentation - item.indentation > 0 : false; + const isCollapsed = !expandNodes.includes(`${item.name}_${index}`); + + // 按钮点击事件 + const operationClick = (e: Event, operationRef: any) => { + // 防止点击按钮触发展开或者合起数据 + e.stopPropagation(); + if (operationRef.current) { + const operationRect = operationRef.current.getBoundingClientRect(); + // 19.2 为图标按钮高度,85 为弹框高度的一半 + dropdownRef.style.setProperty('--content-top', `${operationRect.top + 19.2}px`); + dropdownRef.style.setProperty('--content-left', `${operationRect.left - 85}px`); + } + dropdownRef.classList.toggle(styles['active']); + const attrInfo = { + id: { id }, + itemName: item.name, + attrsName: attrsName, + path: getPath(editableAttrs, index, attrsName), + }; + (dropdownRef as any).attrInfo = attrInfo; + console.log(dropdownRef); + }; + + showAttr.push( +
handleCollapse(item)} + > + {hasChild && } + {`${item.name}`} +
{':'}
+ {item.type === 'string' || item.type === 'number' || item.type === 'undefined' || item.type === 'null' ? ( + <> + { + const nextAttrs = [...editableAttrs]; + const nextItem = { ...item }; + nextItem.value = event.target.value; + nextAttrs[index] = nextItem; + setEditableAttrs(nextAttrs); + }} + onKeyUp={event => { + const value = (event.target as HTMLInputElement).value; + if (event.key === 'Enter') { + if (isDev) { + console.log('post attr change', value); + } else { + const data = buildAttrModifyData(attrsType, attrs, value, item, index, id); + postMessageToBackground(ModifyAttrs, data); + } + } + }} + /> +
+ operationClick(event, operationRef)}> + + +
+ + ) : item.type === 'boolean' ? ( + <> + + {item.value.toString()} + + { + const nextAttrs = [...editableAttrs]; + const nextItem = { ...item }; + nextItem.value = event.target.checked; + nextAttrs[index] = nextItem; + setEditableAttrs(nextAttrs); + if (!isDev) { + const data = buildAttrModifyData(attrsType, attrs, nextItem.value, item, index, id); + postMessageToBackground(ModifyAttrs, data); + } + }} + /> + + ) : ( + <> + + {item.value} + +
+ operationClick(event, operationRef)}> + + +
+ + )} +
+ ); + if (isCollapsed) { + currentIndentation = indentation; + } + }); + + return ( +
+
+ {attrsName} +
+
{showAttr}
+
+ ); +}); + +function ComponentInfo({ name, attrs, parents, id, source, onClickParent }: IComponentInfo) { + const view = useContext(ViewSourceContext) as any; + const viewSource = view.viewSourceFunction.viewSource; + + const pick = useContext(PickElementContext) as any; + const inspectVNode = pick.pickElementFunction.inspectVNode; + const dropdownRef = useRef(null); + + const doViewSource = (id: number) => { + postMessageToBackground(InspectDom, { id }); + setTimeout(function () { + inspectVNode(); + }, 100); + }; + + const doInspectDom = (id: number) => { + postMessageToBackground(InspectDom, { id }); + setTimeout(function () { + inspectVNode(); + }, 100); + }; + + const sourceFormatted = (fileName: string, lineNumber: number) => { + const pathWithoutLastName = /^(.*)[\\/]/; + + let realName = fileName.replace(pathWithoutLastName, ''); + if (/^index\./.test(realName)) { + const fileNameMatch = fileName.match(pathWithoutLastName); + if (fileNameMatch) { + const pathBeforeName = fileNameMatch[1]; + if (pathBeforeName) { + const folderName = pathBeforeName.replace(pathWithoutLastName, ''); + realName = folderName + '/' + realName; + } + } + } + + return `${realName}:${lineNumber}`; + }; + + const copyToConsole = (itemName: string | number, attrsName: string, path: Array) => { + postMessageToBackground(CopyToConsole, { id, itemName, attrsName, path }); + dropdownRef.current.classList.toggle(styles['active']); + }; + + const storeVariable = (attrsName: string, path: Array) => { + postMessageToBackground(StorageValue, { id, attrsName, path }); + dropdownRef.current.classList.toggle(styles['active']); + }; + + return ( +
+
+ {name && ( + <> +
+
{name}
+
+ + + + + + + + )} +
+
+ {Object.keys(attrs).map(attrsType => { + const parsedAttrs = attrs[attrsType]; + if (parsedAttrs && parsedAttrs.length !== 0) { + const attrsName = attrsType.slice(6); // parsedState => State + return ( + + ); + } + return null; + })} +
+ {name && ( +
+
Parents
+ {parents.map(item => ( + + ))} +
+ )} +
+
+ {source && ( + <> +
source: {''}
+
{sourceFormatted(source.fileName, source.lineNumber)}
+ + )} +
+
+
    +
  • + copyToConsole( + (dropdownRef.current as any).attrInfo.itemName, + (dropdownRef.current as any).attrInfo.attrsName, + (dropdownRef.current as any).attrInfo.path + ) + } + > + Copy value to console +
  • +
  • storeVariable((dropdownRef.current as any).attrInfo.attrsName, (dropdownRef.current as any).attrInfo.path)} + > + Store as global variable +
  • +
+
+
+
+ ); +} + +export default memo(ComponentInfo); From b5e1a137d407f7f9cc4f1eeaa5c6e41082455939 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 14 Nov 2023 17:41:27 +0800 Subject: [PATCH 055/108] =?UTF-8?q?[inula-dev-tools]=20=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=E6=A0=91=E9=80=BB=E8=BE=91=E5=90=88=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-dev-tools/src/hooks/FilterTree.ts | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 packages/inula-dev-tools/src/hooks/FilterTree.ts diff --git a/packages/inula-dev-tools/src/hooks/FilterTree.ts b/packages/inula-dev-tools/src/hooks/FilterTree.ts new file mode 100644 index 00000000..6caf8640 --- /dev/null +++ b/packages/inula-dev-tools/src/hooks/FilterTree.ts @@ -0,0 +1,171 @@ +/* + * 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. + */ + +/** + * 过滤树的抽象逻辑 + * 需要知道渲染了哪些数据,过滤的字符串/正则表达式 + * 控制 Tree 组件位置跳转,告知匹配结果 + * 清空搜索框,告知搜索框当前是第几个结果,转跳搜索结果 + * + * 转跳搜索结果的交互逻辑: + * 如果当前页面存在匹配项,页面不动 + * 如果当前页面不存在匹配项,页面转跳到第一个匹配项位置 + * 如果匹配项被折叠,需要展开其父节点。注意只展开当前匹配项的父节点,其他匹配项的父节点不展开 + * 转跳到上一个匹配或下一个匹配项时,如果匹配项被折叠,需要展开其父节点 + * + * 寻找父节点 + * 找到该节点的缩进值和 index 值,在 data 中向上遍历,通过缩进值判断父节点 + */ +import { useState, useRef } from 'openinula'; +import { createRegExp } from '../utils/regExpUtil'; +import { NameObj } from '../parser/parseVNode'; + +type BaseType = { + id: number; + name: NameObj; + indentation: number; +}; + +/** + * 把节点的父节点从收起节点数组中删除,并返回新的收起节点数组 + * + * @param item 需要展开父节点的节点 + * @param data 全部数据 + * @param collapsedNodes 收起节点数据 + * @returns 新的收起节点数组 + */ +function expandItemParent(item: BaseType, data: BaseType[], collapsedNodes: BaseType[]): BaseType[] { + const index = data.indexOf(item); + let currentIndentation = item.indentation; + // 不对原始数据进行修改 + const newCollapsedNodes = [...collapsedNodes]; + for (let i = index - 1; i >= 0; i--) { + const lastData = data[i]; + const lastIndentation = lastData.indentation; + // 缩进更小,找到了父节点 + if (lastIndentation < currentIndentation) { + // 更新缩进值,只招父节点的父节点,避免修改父节点的兄弟节点的展开状态 + currentIndentation = lastIndentation; + const cIndex = newCollapsedNodes.indexOf(lastData); + if (cIndex !== -1) { + newCollapsedNodes.splice(cIndex, 1); + } + } + } + return newCollapsedNodes; +} + +export function FilterTree(props: { data: T[] }) { + const { data } = props; + const [filterValue, setFilterValue] = useState(''); + const [currentItem, setCurrentItem] = useState(null); + const showItemsRef = useRef([]); // 页面展示的 items + const matchItemsRef = useRef([]); //匹配过滤条件的 items + const collapsedNodesRef = useRef([]); // 折叠节点,如果匹配 item 被折叠了,需要展开 + + const matchItems = matchItemsRef.current; + const collapsedNodes = collapsedNodesRef.current; + + const updateCollapsedNodes = (item: BaseType) => { + const newCollapsedNodes = expandItemParent(item, data, collapsedNodes); + // 如果新旧收起节点数组长度不一致,说明存在收起节点 + if (newCollapsedNodes.length !== collapsedNodes.length) { + // 更新引用,确保 VTree 拿到新的 collapsedNodes + collapsedNodesRef.current = newCollapsedNodes; + } + }; + + const onChangeSearchValue = (search: string) => { + const reg = createRegExp(search); + let newCurrentItem = null; + let newMatchItems = []; + if (search !== '') { + const showItems: T[] = showItemsRef.current; + newMatchItems = data.reduce((pre, current) => { + const { name } = current; + if (name && reg && name.itemName.match(reg)) { + pre.push(current); + // 如果当前页面显示的 item 存在匹配项,则把他设置为 currentItem + if (newCurrentItem === null && showItems.includes(current)) { + newCurrentItem = current; + } + } + return pre; + }, []); + + if (newMatchItems.length === 0) { + setCurrentItem(null); + } else { + if (newCurrentItem === null) { + const item = newMatchItems[0]; + // 不处于当前展示页面,需要展开父节点 + updateCollapsedNodes(item); + setCurrentItem(item); + } else { + setCurrentItem(newCurrentItem); + } + } + } else { + setCurrentItem(null); + } + matchItemsRef.current = newMatchItems; + setFilterValue(search); + }; + + const onSelectNext = () => { + const index = matchItems.indexOf(currentItem); + const nextIndex = index + 1; + const item = nextIndex < matchItemsRef.current.length ? matchItems[nextIndex] : matchItems[0]; + // 可能不处于当前展示页面,需要展开父节点 + updateCollapsedNodes(item); + setCurrentItem(item); + }; + + const onSelectLast = () => { + const index = matchItems.indexOf(currentItem); + const last = index - 1; + const item = last >= 0 ? matchItems[last] : matchItems[matchItems.length - 1]; + // 可能不处于当前展示页面,需要展开父节点 + updateCollapsedNodes(item); + setCurrentItem(item); + }; + + const setShowItems = items => { + showItemsRef.current = [...items]; + }; + + const onClear = () => { + onChangeSearchValue(''); + }; + + const setCollapsedNodes = items => { + // 不更新引用,避免子组件的重复渲染 + collapsedNodesRef.current.length = 0; + collapsedNodesRef.current.push(...items); + }; + + return { + filterValue, + onChangeSearchValue, + onClear, + currentItem, + matchItems, + onSelectNext, + onSelectLast, + setShowItems, + collapsedNodes, + setCollapsedNodes, + }; +} From 587f61eec393b7b3c9f00fd0be93211a430b18f4 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Mon, 20 Nov 2023 16:27:53 +0800 Subject: [PATCH 056/108] =?UTF-8?q?[inula-dev-tools]=20Panel=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=90=88=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-dev-tools/src/panel/Panel.less | 117 +++++ packages/inula-dev-tools/src/panel/Panel.tsx | 448 ++++++++++++++++++ packages/inula-dev-tools/src/panel/index.tsx | 19 + packages/inula-dev-tools/src/panel/panel.html | 35 ++ 4 files changed, 619 insertions(+) create mode 100644 packages/inula-dev-tools/src/panel/Panel.less create mode 100644 packages/inula-dev-tools/src/panel/Panel.tsx create mode 100644 packages/inula-dev-tools/src/panel/index.tsx create mode 100644 packages/inula-dev-tools/src/panel/panel.html diff --git a/packages/inula-dev-tools/src/panel/Panel.less b/packages/inula-dev-tools/src/panel/Panel.less new file mode 100644 index 00000000..c68274a1 --- /dev/null +++ b/packages/inula-dev-tools/src/panel/Panel.less @@ -0,0 +1,117 @@ +/* + * 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 '../components/assets.less'; + +.app { + display: flex; + flex-direction: row; + height: 100%; + font-size: @common-font-size; +} + +button { + border: none; + background: none; + padding: 0; +} + +.left { + flex: 0 0 var(--horizontal-percentage); + display: flex; + flex-direction: column; + + .left-top { + border-bottom: @divider-style; + flex: 0 0 @top-height; + display: flex; + align-items: center; + padding-right: 0.4rem; + + .select { + padding: 0 0.5rem 0 0.8rem; + flex: 0 0; + + .Picking { + color: #0088fa; + } + + .StopPicking { + color: #5f6673; + } + + .StopPicking :hover { + color: #23272f; + } + } + + .divider { + flex: 0 0 1px; + margin: 0 0.25rem 0 0.25rem; + border-left: @divider-style; + height: calc(100% - 1rem); + } + + .search { + display: flex; + flex: 1 1 0; + } + + .searchResult { + flex: 0 0; + padding: 0 0.4rem; + } + + .searchAction { + flex: 0 0 1rem; + height: 1rem; + color: @arrow-color; + &:hover { + color: @hover-color; + } + } + } + + .left-bottom { + flex: 1; + height: 0; + } +} + +.resizeBar { + flex: 0 0 0; + position: relative; + resize: horizontal; + .resizeLine { + position: absolute; + left: -2px; + width: 5px; + height: 100%; + cursor: ew-resize; + } +} + +.right { + flex: 3; + overflow-x: hidden; + overflow-y: auto; + border-left: @divider-style; +} + +input { + outline: none; + border-width: 0; + padding: 0; +} diff --git a/packages/inula-dev-tools/src/panel/Panel.tsx b/packages/inula-dev-tools/src/panel/Panel.tsx new file mode 100644 index 00000000..66a34896 --- /dev/null +++ b/packages/inula-dev-tools/src/panel/Panel.tsx @@ -0,0 +1,448 @@ +/* + * 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 { + useState, + useEffect, + useRef, + memo, + useMemo, + useCallback, + useReducer, +} from 'openinula'; +import VTree, { IData } from '../components/VTree'; +import Search from '../components/Search'; +import ComponentInfo from '../components/ComponentInfo'; +import styles from './Panel.less'; +import Select from '../svgs/Select'; +import { FilterTree } from '../hooks/FilterTree'; +import Close from '../svgs/Close'; +import Arrow from '../svgs/Arrow'; +import { + AllVNodeTreeInfos, + RequestComponentAttrs, + ComponentAttrs, + PickElement, + StopPickElement, +} from '../utils/constants'; +import { + addBackgroundMessageListener, + initBackgroundConnection, + postMessageToBackground, + removeBackgroundMessageListener, +} from '../panelConnection'; +import { IAttr } from '../parser/parseAttr'; +import { NameObj } from '../parser/parseVNode'; +import { createLogger } from '../utils/logUtil'; +import type { Source } from '../../../inula/src/renderer/Types'; +import ViewSourceContext from '../utils/ViewSource'; +import PickElementContext from '../utils/PickElement'; +import Discover from '../svgs/Discover'; + +type ResizeActionType = 'START_RESIZE' | 'SET_HORIZONTAL_PERCENTAGE'; + +type ResizeAction = { + type: ResizeActionType; + payload: any; +}; + +type ResizeState = { + horizontalPercentage: number; + isResizing: boolean; +}; + +const logger = createLogger('panelApp'); +let maxDeep = 0; +const parseVNodeData = (rawData, idToTreeNodeMap, nextIdToTreeNodeMap) => { + const indentationMap: { + [id: string]: number; + } = {}; + const data: IData[] = []; + let i = 0; + while (i < rawData.length) { + const id = rawData[i] as number; + i++; + const name = rawData[i] as NameObj; + i++; + const parentId = rawData[i] as string; + i++; + const userKey = rawData[i] as string; + i++; + const indentation = parentId === '' ? 0 : indentationMap[parentId] + 1; + maxDeep = maxDeep >= indentation ? maxDeep : indentation; + indentationMap[id] = indentation; + const lastItem = idToTreeNodeMap[id]; + if (lastItem) { + // 由于 diff 算法限制,一个 vNode 的 name,userKey,indentation 属性不会发生变化 + // 但是在跳转到新页面时, id 值重置,此时原有 id 对应的节点都发生了变化,需要更新 + // 为了让架构尽可能简单,不区分是否是页面挑战,所以每次都需要重新赋值 + nextIdToTreeNodeMap[id] = lastItem; + lastItem.name = name; + lastItem.indentation = indentation; + lastItem.userKey = userKey; + data.push(lastItem); + } else { + const item = { + id, + name, + indentation, + userKey, + }; + nextIdToTreeNodeMap[id] = item; + data.push(item); + } + } + return data; +}; + +const getParents = (item: IData | null, parsedVNodeData: IData[]) => { + const parents: IData[] = []; + if (item) { + const index = parsedVNodeData.indexOf(item); + let indentation = item.indentation; + for (let i = index; i >= 0; i--) { + const last = parsedVNodeData[i]; + const lastIndentation = last.indentation; + if (lastIndentation < indentation) { + parents.push(last); + indentation = lastIndentation; + } + } + } + return parents; +}; + +interface IIdToNodeMap { + [id: number]: IData; +} + +/** + * 设置 dev tools 页面左树占比 + * + * @param {null | HTMLElement} resizeElement 要改变宽度的页面元素 + * @param {number} percentage 宽度占比 + */ +const setResizePCTForElement = ( + resizeElement: null | HTMLElement, + percentage: number +): void => { + if (resizeElement !== null) { + resizeElement.style.setProperty( + '--horizontal-percentage', + `${percentage}` + ); + } +}; + +function resizeReducer(state: ResizeState, action: ResizeAction): ResizeState { + switch (action.type) { + case "START_RESIZE": + return { + ...state, + isResizing: action.payload, + }; + case "SET_HORIZONTAL_PERCENTAGE": + return { + ...state, + horizontalPercentage: action.payload, + }; + default: + return state; + } +} + +function initResizeState(): ResizeState { + const horizontalPercentage = 0.62; + + return { + horizontalPercentage, + isResizing: false, + }; +} + +function Panel({ viewSource, inspectVNode }) { + const [parsedVNodeData, setParsedVNodeData] = useState([]); + const [componentAttrs, setComponentAttrs] = useState<{ + parsedProps?: IAttr[]; + parsedState?: IAttr[]; + parsedHooks?: IAttr[]; + }>({}); + const [selectComp, setSelectComp] = useState(null); + const [isPicking, setPicking] = useState(false); + const [source, setSource] = useState(null); + const idToTreeNodeMapref = useRef({}); + const [state, dispatch] = useReducer( + resizeReducer, + null, + initResizeState + ); + const pageRef = useRef(null); + const treeRef = useRef(null); + + const { horizontalPercentage } = state; + const { + filterValue, + onChangeSearchValue: setFilterValue, + onClear, + currentItem, + matchItems, + onSelectNext, + onSelectLast, + setShowItems, + collapsedNodes, + setCollapsedNodes, + } = FilterTree({ data: parsedVNodeData }); + + useEffect(() => { + if (isDev) { + // const nextIdToTreeNodeMap: IIdToNodeMap = {}; + } else { + const handleBackgroundMessage = message => { + const { payload } = message; + // 对象数据只是记录了引用,内容可能在后续被修改,打印字符串可以获取当前真正内容,不被后续修改影响 + logger.info(JSON.stringify(payload)); + if (payload) { + const {type, data} = payload; + if (type === AllVNodeTreeInfos) { + const idToTreeNodeMap = idToTreeNodeMapref.current; + const nextIdToTreeNodeMap: IIdToNodeMap = {}; + const allTreeData = data.reduce((pre, current) => { + const parsedTreeData = parseVNodeData( + current, + idToTreeNodeMap, + nextIdToTreeNodeMap + ); + return pre.concat(parsedTreeData); + }, []); + idToTreeNodeMapref.current = nextIdToTreeNodeMap; + setParsedVNodeData(allTreeData); + if (selectComp) { + postMessageToBackground(RequestComponentAttrs, selectComp.id); + } + } else if (type === ComponentAttrs) { + const { parsedProps, parsedState, parsedHooks, src } = data; + setComponentAttrs({ + parsedProps, + parsedState, + parsedHooks, + }); + setSource(src); + } else if (type === StopPickElement) { + setPicking(false); + } else if (type === PickElement) { + const target = Object.values(idToTreeNodeMapref.current).find(({ id }) => id == data); + setSelectComp(target); + } + } + }; + // 在页面渲染后初始化连接 + initBackgroundConnection('panel'); + // 监听 background 消息 + addBackgroundMessageListener(handleBackgroundMessage); + return () => { + removeBackgroundMessageListener(handleBackgroundMessage); + }; + } + }, [selectComp]); + + useEffect(() => { + const treeElement = treeRef.current; + + setResizePCTForElement(treeElement, horizontalPercentage * 100); + }, []); + + const handleSearchChange = (str: string) => { + setFilterValue(str); + }; + + const handleSelectComp = (item: IData) => { + setSelectComp(item); + if (isDev) { + // setComponentAttrs({}); + } else { + postMessageToBackground(RequestComponentAttrs, item.id); + } + }; + + const handleClickParent = useCallback((item: IData) => { + setSelectComp(item); + }, []); + + const onRendered = info => { + setShowItems(info.visibleItems); + }; + + const parents = useMemo( + () => getParents(selectComp, parsedVNodeData), + [selectComp, parsedVNodeData] + ); + + const viewSourceFunction = useMemo( + () => ({ + viewSource: viewSource || null, + }), + [viewSource] + ); + + // 选择页面元素对应到 dev tools + const pickElementFunction = useMemo( + () => ({ + inspectVNode: inspectVNode || null, + }), + [inspectVNode] + ); + + // 选择页面元素图标样式 + let pickClassName; + if (isPicking) { + pickClassName = styles.Picking; + } else { + pickClassName = styles.StopPicking; + } + + const MINIMUM_SIZE = 50; + const { isResizing } = state; + const doResize = () => dispatch({ type: 'START_RESIZE', payload: true }); + let onResize; + let stopResize; + if (isResizing) { + stopResize = () => dispatch({ type: 'START_RESIZE', payload: false }); + + onResize = event => { + // 设置横向 resize 百分比区域(左树部分) + const treeElement = treeRef.current; + // 整个页面(左树部分加节点详情部分),要拿到页面宽度,防止 resize 时移出页面 + const pageElement = pageRef.current; + + if (isResizing || pageElement === null || treeElement === null) { + return; + } + + // 左移时防止左树移出页面 + event.preventDefault(); + + const { width, left } = pageElement.getBoundingClientRect(); + + const mouseAbscissa = event.clientX - left; + + const pageSizeMin = MINIMUM_SIZE; + const pageSizeMax = width-MINIMUM_SIZE; + + const isMouseInPage = mouseAbscissa > pageSizeMin && mouseAbscissa < pageSizeMax; + + if (isMouseInPage) { + const resizedElementWidth = width; + const actionType = 'SET_HORIZONTAL_PERCENTAGE'; + const percentage = (mouseAbscissa / resizedElementWidth) * 100; + + setResizePCTForElement(treeElement, percentage); + + dispatch({ + type: actionType, + payload: mouseAbscissa / resizedElementWidth, + }); + } + }; + } + + return ( + + +
+
+
+
+ + {' | '} + { + e.stopPropagation(); + + chrome.runtime.sendMessage({ + type: 'INULA_DEV_TOOLS', + payload: { + type: otherTypes.SET_PERSISTENT, + tabId: chrome.devtools.inspectedWindow.tabId, + persistent: !persistent, + }, + from: DevToolPanel, + }); + + setPersistent(!persistent); + }} + > + Persistent events + + {' | '} + + {eventFilter['message.data.store.id'] ? ( + + {' | '} + { + setNextStore(eventFilter['message.data.store.id']); + }} + >{` Displaying: [${eventFilter['message.data.store.id']}] `} + + + ) : null} +
+
+ + {Object.values(eventTypes).map(eventType => { + return ( + + ); + })} +
+ { + const message = data.event.message; + return { + type: data.type, + store: { + actions: Object.fromEntries( + Object.entries(message.data.store.$config.actions).map( + ([id, action]) => { + return [ + id, + (action as string).replace(/\{.*}/gms, '{...}').replace('function ', ''), + ]; + } + ) + ), + computed: Object.fromEntries( + Object.keys(message.data.store.$c).map(key => [ + key, + message.data.store.expanded[key], + ]) + ), + state: message.data.store.$s, + id: message.data.store.id, + }, + // data: omit(data, 'storeClick', 'additionalData'), + }; + }} + search={eventFilter.fulltext ? eventFilter.fulltext : ''} + /> + + ); +} diff --git a/packages/inula-dev-tools/src/panelX/Modal.tsx b/packages/inula-dev-tools/src/panelX/Modal.tsx new file mode 100644 index 00000000..267d5493 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/Modal.tsx @@ -0,0 +1,103 @@ +/* + * 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 Inula, { useRef, useState } from 'openinula'; + +export function Modal({ + closeModal, + then, + children, +}: { + closeModal: () => void; + then: (value: any) => void; + children?: any[]; +}) { + const inputRef = useRef(null); + const [error, setError] = useState(null); + + setTimeout(() => { + inputRef.current.focus(); + inputRef.current.value = ''; + }, 10); + + const tryGatherData = () => { + let data; + try { + data = eval(inputRef.current.value); + } catch (err) { + setError(err); + return; + } + + if (then) { + then(data); + } + }; + + return ( +
+
+

{children}

+

+ { + if (key === 'Enter') { + tryGatherData(); + } + }} + /> +

+ {error ?

Variable parsing error

: null} +

+ + +

+
+
+ ); +} diff --git a/packages/inula-dev-tools/src/panelX/PanelX.less b/packages/inula-dev-tools/src/panelX/PanelX.less new file mode 100644 index 00000000..04656c3c --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/PanelX.less @@ -0,0 +1,197 @@ +/* + * 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 '../components/assets.less'; + +.displayData { + background-color: rgb(241, 243, 244); +} + +.app { + display: flex; + flex-direction: row; + height: 100%; + font-size: @common-font-size; +} + +div.wrapper { + margin: 15px; + position: relative; + width: calc(100% - 30px); + display: block; +} + +div.table { + display: table; + vertical-align: top; + width: calc(100%); + background-color: white; + position: relative; +} + +div.row { + display: table-row; + + &:nth-child(2n + 1) { + background-color: rgb(241, 243, 244); + .default { + background-color: rgb(241, 243, 244); + } + } +} + +div.cell { + display: table-cell; + cursor: pointer; + padding: 5px; +} + +div.half { + width: calc(50% - 8px); + float: left; +} + +div.header { + background-color: rgb(241, 243, 244); + font-weight: bold; +} + +div.row.active { + background-color: #00a; + color: white; +} + +button.tab { + border: 1px solid grey; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + &.active { + border-bottom: none; + color: black; + font-weight: bold; + background-color: white; + } +} + +span.highlighted { + background-color: #ff0; +} + +.grey { + color: grey; +} + +.red { + color: #a00; +} + +.blue { + color: #00a; +} + +.purple { + color: #909; +} + +.bold { + font-weight: bold; +} + +.link { + font-weight: bold; + text-decoration: underline; + cursor: pointer; + color: #00a; +} + +.compositeInput { + background-color: white; + border: 1px solid grey; + display: inline-block; + border-radius: 0; + padding: 5px; + &.left { + border-right: 0; + margin-right: 0; + padding-right: 0; + } + + &.right { + border-left: 0; + margin-left: 0; + } + + &:focus-visible { + outline: none; + } +} + +.filterButton { + background-color: transparent; + padding: 5px; + border-radius: 5px; + border: 0; + &.active { + background-color: #ddd; + } +} + +.added { + background-color: #afa; + &::before { + font-weight: bold; + color: #0a0; + } +} + +.deleted { + background-color: #faa; + text-decoration-line: line-through; + &::before { + font-weight: bold; + color: #a00; + } +} + +.changed { + background-color: #ffa; + &::before { + font-weight: bold; + color: #ca0; + } +} + +.default { + background-color: white; +} + +.floatingButton { + right: 5px; + position: absolute; + height: 17px; + width: 17px; + font-size: 10px; + padding: 0; + cursor: pointer; +} + +.scrollable { + max-height: calc(100vh - 65px); + overflow: auto; + + div.row { + display: block; + } +} diff --git a/packages/inula-dev-tools/src/panelX/PanelX.tsx b/packages/inula-dev-tools/src/panelX/PanelX.tsx new file mode 100644 index 00000000..b3038c86 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/PanelX.tsx @@ -0,0 +1,84 @@ +/* + * 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 { useState } from 'openinula'; +import EventLog from './EventLog'; +import Stores from './Stores'; +import styles from './PanelX.less'; + +export default function PanelX() { + const [active, setActive] = useState('stores'); + const [nextStoreId, setNextStoreId] = useState(null); + const [eventFilter, setEventFilter] = useState({}); + + function showFilterEvents(filter) { + setActive('events'); + setEventFilter(filter); + } + + const tabs = [ + { + id: 'stores', + title: 'Stores', + getComponent: () => ( + + ), + }, + { + id: 'events', + title: 'Events', + getComponents: () => ( + { + setNextStoreId(id); + setActive('stores'); + }} + setEventFilter={setEventFilter} + eventFilter={eventFilter} + /> + ), + }, + ]; + + return ( +
+
+ {tabs.map(tab => + tab.id === active ? ( + + ) : ( + + ) + )} +
+ {tabs.find(item => item.id === active).getComponent()} +
+ ); +} diff --git a/packages/inula-dev-tools/src/panelX/Stores.tsx b/packages/inula-dev-tools/src/panelX/Stores.tsx new file mode 100644 index 00000000..aa7c60e5 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/Stores.tsx @@ -0,0 +1,74 @@ +/* + * 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 { useState, useEffect } from 'openinula'; +import { + initBackgroundConnection, + addBackgroundMessageListener, + removeBackgroundMessageListener, + postMessageToBackground, +} from '../panelConnection'; +import { Table } from './Table'; +import { omit, sendMessage } from './utils'; +import styles from './PanelX.less'; +import { Highlight, RemoveHighlight } from '../utils/constants'; +import { ActionRunner } from './ActionRunner'; +import { Tree } from './Tree'; + +export default function Stores({ nextStoreId, showFilteredEvents }) { + const [stores, setStores] = useState([]); + const [initialized, setInitialized] = useState(false); + + if (!initialized) { + setTimeout(() => { + sendMessage({ + type: 'inulax getStores', + tabId: chrome.devtools.inspectedWindow.tabId, + }); + }, 100); + } + + useEffect(() => { + const listener = message => { + if (message.payload.type.startsWith('inulax')) { + // 过滤 inula 消息 + if (message.payload.type === 'inulax stores') { + // Stores 更新 + setStores(message.payload.stores); + setInitialized(true); + } else if (message.payload.type === 'inulax flush stores') { + // Flush store + sendMessage({ + type: 'inulax getStores', + tabId: chrome.devtools.inspectedWindow.tabId, + }); + } else if (message.payload.type === 'inulax observed components') { + // observed components 更新 + setStores( + stores.map(store => { + store.observedComponents = message.payload.data[store.id] || []; + return store; + }) + ); + } + } + }; + initBackgroundConnection('panel'); + addBackgroundMessageListener(listener); + return () => { + removeBackgroundMessageListener(listener); + }; + }); +} diff --git a/packages/inula-dev-tools/src/panelX/Table.tsx b/packages/inula-dev-tools/src/panelX/Table.tsx new file mode 100644 index 00000000..81ab7248 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/Table.tsx @@ -0,0 +1,164 @@ +/* + * 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 { useState } from 'openinula'; +import {Tree} from './Tree'; +import styles from './PanelX.less'; + +type displayKeysType = [string, string][]; + +export function Table({ + data, + dataKey = 'id', + displayKeys, + activate, + displayDataProcessor, + search = '', +}: { + data; + dataKey: string; + displayKeys: displayKeysType; + activate?: { + [key: string]: any; + }; + displayDataProcessor: (data: { [key: string]: any }) => { + [key: string]: any; + }; + search: string; +}) { + const [keyToDisplay, setKeyToDisplay] = useState(false); + const [manualOverride, setManualOverride] = useState(false); + let displayRow = null; + + if (!manualOverride && activate) { + data.forEach(item => { + if (displayRow) { + return; + } + let notMatch = false; + Object.entries(activate).forEach(([key, value]) => { + if (notMatch) { + return; + } + if (item[key] !== value) { + notMatch = true; + } + }); + if (notMatch) { + return; + } + displayRow = item; + }); + } else if (manualOverride && keyToDisplay) { + data.forEach(item => { + if (displayRow) { + return; + } + if (item[dataKey] === keyToDisplay) { + displayRow = item; + } + }); + } + + if (displayRow) { + const [attr, title] = displayKeys[0]; + return ( +
+
+
+
{title}
+
+
+ + {data.map(row => ( +
{ + setManualOverride(true); + setKeyToDisplay( + keyToDisplay === row[dataKey] ? null : row[dataKey] + ); + }} + > +
{row?.[attr] || ''}
+
+ ))} +
+
+ +
+
+
+ Data: + +
+
+
+ +
+
+ +
+
+
+
+
+ ); + } else { + return ( +
+
+
+ {displayKeys.map(([key, title]) => ( +
{title}
+ ))} +
+ {data.map(item => ( +
{ + setManualOverride(true); + setKeyToDisplay(item[dataKey]); + }} + className={styles.row} + > + {displayKeys.map(([key, title]) => ( +
{item[key]}
+ ))} +
+ ))} +
+
+ ); + } +} diff --git a/packages/inula-dev-tools/src/panelX/Tree.tsx b/packages/inula-dev-tools/src/panelX/Tree.tsx new file mode 100644 index 00000000..3091c1a4 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/Tree.tsx @@ -0,0 +1,230 @@ +/* + * 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 { useState } from 'openinula'; +import styles from './PanelX.less'; +import {Modal} from './Modal'; +import { displayValue, omit } from './utils'; + +export function Tree({ + data, + indent = 0, + index = '', + expand = false, + search = '', + forcedExpand = false, + onEdit = null, + omitAttrs = [], + className, + forcedLabel = null, +}: { + data: any; + indent?: number; + index?: string | number; + expand?: boolean; + search?: string; + forcedExpand?: boolean; + className?: string | undefined + omitAttrs?: string[]; + onEdit?: (path: any[], value: any) => void | null; + forcedLabel?: string | number | null; +}) { + const [expanded, setExpanded] = useState(expand); + const [modal, setModal] = useState(false); + const isArray = Array.isArray(data); + const isObject = data && !isArray && typeof data === 'object'; + const isSet = isObject && data?._type === 'Set'; + const isWeakSet = isObject && data?._type === 'WeakSet'; + const isMap = isObject && data?._type === 'Map'; + const isWeakMap = isObject && data?._type === 'WeakMap'; + const isVNode = isObject && data.vtype; + const canBeExpanded = isArray || (isObject && !isWeakSet && !isWeakMap); + + if (isObject && omitAttrs?.length) { + data = omit(data, ...omitAttrs); + } + + return canBeExpanded ? ( +
{ + e.stopPropagation(); + }} + > + { + setExpanded(!expanded); + }} + > + {new Array(Math.max(indent, 0)).fill( )} + {forcedExpand || isVNode ? null : expanded ? ( + + ) : ( + + )} + {index === 0 || index ? ( + <> + {displayValue(index, search)}: + + ) : ( + '' + )} + {forcedLabel + ? forcedLabel + : expanded + ? isVNode + ? null + : Array.isArray(data) + ? `Array(${data.length})` + : isMap + ? `Map(${data.entries.length})` + : isSet + ? `Set(${data.values.length})` + : '{ ... }' + : isWeakMap + ? 'WeakMap()' + : isWeakSet + ? 'WeakSet()' + : isMap + ? `Map(${data.entries.length})` + : isSet + ? `Set(${data.values.length})` + : Array.isArray(data) + ? `Array(${data.length})` + : '{ ... }'} + + {expanded || isVNode ? ( + isArray ? ( + <> + {data.map((value, index) => { + return ( +
+ { + onEdit(path.concat([index]), val); + } + : null + } + /> +
+ ); + })} + + ) : isVNode ? ( + data + ) : isMap ? ( +
+ {data.entries.map(([key, value]) => { + return ( + + ); + })} +
+ ) : isSet ? ( + data.values.map(item => { + return ( +
+ +
+ ); + }) + ) : ( + Object.entries(data).map(([key, value]) => { + return ( +
+ { + onEdit(path.concat([key]), val); + } + : null + } + /> +
+ ); + }) + ) + ) : ( + '' + )} +
+ ) : ( +
+ {new Array(indent).fill( )} + + {typeof index !== 'undefined' ? ( + <> + {displayValue(index, search)}: + + ) : ( + '' + )} + {displayValue(data, search)} + {onEdit && !isWeakSet && !isWeakMap ? ( // TODO: editable weak set and map + <> + { + setModal(true); + }} + > + ☼ + + {onEdit && modal ? ( + { + setModal(false); + }} + then={data => { + onEdit([], data); + setModal(false); + }} + > +

Edit value:

{index} +
+ ) : null} + + ) : null} +
+
+ ); +} diff --git a/packages/inula-dev-tools/src/panelX/index.tsx b/packages/inula-dev-tools/src/panelX/index.tsx new file mode 100644 index 00000000..7e46144c --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/index.tsx @@ -0,0 +1,18 @@ +/* + * 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 PanelX from './PanelX'; + +export default PanelX; diff --git a/packages/inula-dev-tools/src/panelX/panelX.html b/packages/inula-dev-tools/src/panelX/panelX.html new file mode 100644 index 00000000..4a211dcf --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/panelX.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + +
+ + + + diff --git a/packages/inula-dev-tools/src/panelX/utils.tsx b/packages/inula-dev-tools/src/panelX/utils.tsx new file mode 100644 index 00000000..1f0d807f --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/utils.tsx @@ -0,0 +1,228 @@ +/* + * 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 Inula from 'openinula'; +import styles from './PanelX.less'; +import { DevToolPanel } from '../utils/constants'; + +export function highlight(source, search) { + if (!search || !source?.split) { + return source; + } + + const parts = source.split(search); + const result = []; + + for (let i= 0; i < parts.length * 2 - 1; i++) { + if (i % 2) { + result.push({search}); + } else { + result.push(parts[i / 2]); + } + } + + return result; +} + +export function displayValue(val: any, search = '') { + if (typeof val === 'boolean') { + return ( + + {highlight(val ? 'true' : 'false', search)} + + ); + } + + if (val === '') { + return {'""'}; + } + + if (typeof val === 'undefined') { + return {highlight('undefined', search)}; + } + + if (val === 'null') { + return {highlight('null', search)}; + } + + if (typeof val === 'string') { + if (val.match(/^function\s?\(/)) { + return ( + + ƒ + {highlight( + val.match(/^function\s?\([\w,]*\)/g)[0].replace(/^function\s?/, ''), + search + )} + + ); + } + return "{highlight(val, search)}"; + } + if (typeof val === 'number') { + return {highlight('' + val, search)}; + } + if (typeof val === 'function') { + const args = val.toString().match(/^function\s?\([\w,]*\)/g)[0].replace(/^function\s?/, ''); + return ( + + ƒ + {highlight(args, search)} + + ); + } + if (typeof val === 'object') { + if (val?._type === 'WeakSet') { + return WeakSet(); + } + + if (val?._type === 'WeakMap') { + return WeakMap(); + } + } +} + +export function fullTextSearch(value, search) { + if (!value) { + return false; + } + + if (Array.isArray(value)) { + return value.some(val => fullTextSearch(val, search)); + } + + if (typeof value === 'object') { + if (value?._type === 'Set') { + return value.values.some(val => fullTextSearch(val, search)); + } + if (value?._type === 'Map') { + return value.entries.some( + (key, val) => fullTextSearch(key, search) || fullTextSearch(val, search) + ); + } + return Object.values(value).some(val => fullTextSearch(val, search)); + } + + return value.toString().includes(search); +} + +export function omit(obj, ...attrs) { + const res = { ...obj }; + attrs.forEach(attr => delete res[attr]); + return res; +} + +export function stringify(data) { + if (typeof data === 'string' && data.startsWith('function(')) { + return ( + + ƒ + {data.match(/^function\([\w,]*\)/g)[0].substring(8)} + + ); + } + + if (!data) { + return displayValue(data); + } + + if (Array.isArray(data)) { + return `Array(${data.length})`; + } + + if (typeof data === 'object') { + return `{${Object.entries(data).map(([key, value]) => { + if (typeof value === 'string' && value.startsWith('function(')) { + return ( + + {key} + + ƒ + {value.match(/^function\([\w,]*\)/g)[0].substring(8)} + + + ); + } + if (!value) { + return ( + + {key}:{displayValue(value)} + + ); + } + if (Array.isArray(value)) { + return ( + + {key}:{' '} + {`Array(${value.length})`} + + ); + } + if (typeof value === 'object') { + if ((value as any)?._type === 'WeakSet') { + return ( + + {key}: {'WeakSet()'} + + ); + } + if ((value as any)?._type === 'WeakMap') { + return ( + + {key}: {'WeakMap'} + + ); + } + if ((value as any)?._type === 'Set') { + return ( + + {key}:{' '} + {`Set(${(value as Set).size})`} + + ); + } + if ((value as any)?._type === 'Map') { + return ( + + {key}:{' '} + {`Map(${(value as Map).size})`} + + ); + } + + // object + return ( + + {key}: {'{...}'} + + ); + } + return ( + + {key}: {displayValue(value)} + + ); + })}}`; + } + return data; +} + +export function sendMessage(payload) { + chrome.runtime.sendMessage({ + type: 'INULA_DEV_TOOLS', + payload, + from: DevToolPanel, + }); +} From e6265ec7885c145acacb6f796409c28f8bddeb3c Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 28 Nov 2023 16:11:59 +0800 Subject: [PATCH 058/108] =?UTF-8?q?[inula-dev-tools]=20contentScript?= =?UTF-8?q?=20=E5=90=88=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/contentScript/index.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 packages/inula-dev-tools/src/contentScript/index.ts diff --git a/packages/inula-dev-tools/src/contentScript/index.ts b/packages/inula-dev-tools/src/contentScript/index.ts new file mode 100644 index 00000000..a77affdc --- /dev/null +++ b/packages/inula-dev-tools/src/contentScript/index.ts @@ -0,0 +1,68 @@ +/* + * 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 { injectSrc, injectCode } from '../utils/injectUtils'; +import { checkMessage } from '../utils/transferUtils'; +import { DevToolContentScript, DevToolHook, DevToolBackground } from '../utils/constants'; +import { changeSource } from '../utils/transferUtils'; + +// 页面的 window 对象不能直接通过 contentScript 代码修改,只能通过添加 js 代码往页面 window 注入 hook +const rendererURL = chrome.runtime.getURL('/injector.js'); +if (window.performance.getEntriesByType('navigation')) { + const entryType = (window.performance.getEntriesByType('navigation')[0] as any).type; + if (entryType === 'navigate') { + injectSrc(rendererURL); + } else if (entryType === 'reload' && !(window as any).__INULA_DEV_HOOK__) { + let rendererCode; + const request = new XMLHttpRequest(); + request.addEventListener('load', function () { + rendererCode = this.responseText; + }); + request.open('GET', rendererURL, false); + request.send(); + injectCode(rendererCode); + } +} + +// 监听来自页面的信息 +window.addEventListener( + 'message', + event => { + // 只监听来自本页面的消息 + if (event.source !== window) { + return; + } + + const data = event.data; + if (checkMessage(data, DevToolHook)) { + changeSource(data, DevToolContentScript); + // 传递给 background + chrome.runtime.sendMessage(data); + } + }, + false +); + +// 监听来自 background 的消息 +chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) { + // 该方法可以监听页面 contentScript 和插件的消息 + // 没有 tab 信息说明消息来自插件 + if (!sender.tab && checkMessage(message, DevToolBackground)) { + changeSource(message, DevToolContentScript); + // 传递消息给页面 + window.postMessage(message, '*'); + } + sendResponse({ status: 'ok' }); +}); From 12fb1d43331c5b1ee96e26ccab125792832f4fb7 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 28 Nov 2023 16:32:05 +0800 Subject: [PATCH 059/108] =?UTF-8?q?[inula-dev-tools]=20=E8=B0=83?= =?UTF-8?q?=E8=AF=95=E5=B7=A5=E5=85=B7=E4=B8=BB=E5=87=BD=E6=95=B0=E5=90=88?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-dev-tools/src/main/index.ts | 101 ++++++++++++++++++++ packages/inula-dev-tools/src/main/main.html | 30 ++++++ 2 files changed, 131 insertions(+) create mode 100644 packages/inula-dev-tools/src/main/index.ts create mode 100644 packages/inula-dev-tools/src/main/main.html diff --git a/packages/inula-dev-tools/src/main/index.ts b/packages/inula-dev-tools/src/main/index.ts new file mode 100644 index 00000000..2de25903 --- /dev/null +++ b/packages/inula-dev-tools/src/main/index.ts @@ -0,0 +1,101 @@ +/* + * 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 { render, createElement } from 'openinula'; +import Panel from '../panel/Panel'; +import PanelX from '../panelX/PanelX'; + +let panelCreated = false; + +const viewSource = () => { + setTimeout(() => { + chrome.devtools.inspectedWindow.eval(` + if (window.$type != null) { + if ( + window.$type && + window.$type.prototype && + window.$type.prototype.render + ) { + // 类组件 + inspect(window.$type.prototype.render); + } else { + // 函数组件 + inspect(window.$type); + } + } + `); + }, 100); +}; + +const inspectVNode = () => { + chrome.devtools.inspectedWindow.eval( + ` + window.__INULA_DEV_HOOK__ && window.__INULA_DEV_HOOK__.$0 !== $0 + ? (inspect(window.__INULA_DEV_HOOK__.$0.realNode), true) + : false + `, + (_, error) => { + if (error) { + console.error(error); + } + } + ); +}; + +let currentPanel = null; + +chrome.devtools.inspectedWindow.eval( + 'window.__INULA_DEV_HOOK__', + function (isInula, error) { + if (!isInula || panelCreated) { + return; + } + + panelCreated = true; + chrome.devtools.panels.create( + 'Inula', + '', + 'panel.html', + (extensionPanel) => { + extensionPanel.onShown.addListener((panel) => { + if (currentPanel === panel) { + return; + } + currentPanel = panel; + const container = panel.document.getElementById('root'); + const element = createElement(Panel, { viewSource, inspectVNode }); + render(element, container); + }); + } + ); + + chrome.devtools.panels.create( + 'InulaX', + '', + 'panelX.html', + (extensionPanel) => { + extensionPanel.onShown.addListener((panel) => { + if (currentPanel === panel) { + return; + } + currentPanel = panel; + const container = panel.document.getElementById('root'); + const element = createElement(PanelX, {}); + render(element, container); + }); + } + ); + } +); diff --git a/packages/inula-dev-tools/src/main/main.html b/packages/inula-dev-tools/src/main/main.html new file mode 100644 index 00000000..210cec23 --- /dev/null +++ b/packages/inula-dev-tools/src/main/main.html @@ -0,0 +1,30 @@ + + + + + + + + + + +
+

Inula dev tools!

+
+ + + From dd09706849b4c2ff1c753c95d416900b08aa12a3 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 28 Nov 2023 16:34:24 +0800 Subject: [PATCH 060/108] =?UTF-8?q?[inula-dev-tools]=20=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-dev-tools/src/utils/injectUtils.ts | 2 +- packages/inula/src/inulax/devtools/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/inula-dev-tools/src/utils/injectUtils.ts b/packages/inula-dev-tools/src/utils/injectUtils.ts index 7b25df96..e5614388 100644 --- a/packages/inula-dev-tools/src/utils/injectUtils.ts +++ b/packages/inula-dev-tools/src/utils/injectUtils.ts @@ -38,7 +38,7 @@ export function injectSrc(src) { ).appendChild(script); } -function injectCode(code) { +export function injectCode(code) { const script = document.createElement('script'); script.textContent = code; diff --git a/packages/inula/src/inulax/devtools/index.ts b/packages/inula/src/inulax/devtools/index.ts index 10c71a9e..01dc5c37 100644 --- a/packages/inula/src/inulax/devtools/index.ts +++ b/packages/inula/src/inulax/devtools/index.ts @@ -16,7 +16,7 @@ import { isMap, isSet, isWeakMap, isWeakSet } from '../CommonUtils'; import { getStore, getAllStores } from '../store/StoreHandler'; import { OBSERVED_COMPONENTS } from './constants'; -import { VNode } from "../../renderer/vnode/VNode"; +import { VNode } from '../../renderer/vnode/VNode'; const sessionId = Date.now(); From 55af46db34cd14898fe163d383b751b29f9d0ab0 Mon Sep 17 00:00:00 2001 From: huangxuan Date: Thu, 30 Nov 2023 15:38:05 +0800 Subject: [PATCH 061/108] =?UTF-8?q?[inulax]=20connect=20API=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81forwardRef=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula/src/inulax/adapters/reduxReact.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/inula/src/inulax/adapters/reduxReact.ts b/packages/inula/src/inulax/adapters/reduxReact.ts index 51650f98..160dec37 100644 --- a/packages/inula/src/inulax/adapters/reduxReact.ts +++ b/packages/inula/src/inulax/adapters/reduxReact.ts @@ -17,6 +17,7 @@ import { useState, useContext, useEffect, useRef } from '../../renderer/hooks/Ho import { createContext } from '../../renderer/components/context/CreateContext'; import { createElement } from '../../external/JSXElement'; import type { ReduxStoreHandler, ReduxAction, BoundActionCreator } from './redux'; +import { forwardRef } from '../../renderer/components/ForwardRef'; const DefaultContext = createContext(null); type Context = typeof DefaultContext; @@ -90,6 +91,11 @@ type MergePropsP = ( type WrappedComponent = (props: OwnProps) => ReturnType; type OriginalComponent = (props: MergedProps) => ReturnType; type Connector = (Component: OriginalComponent) => WrappedComponent; +type ConnectOption = { + areStatesEqual?: (oldState: State, newState: State) => boolean; + context?: Context; + forwardRef?: boolean +} export function connect( mapStateToProps: MapStateToPropsP = () => ({} as StateProps), @@ -99,10 +105,7 @@ export function connect( dispatchProps, ownProps ): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps } as unknown as MergedProps), - options?: { - areStatesEqual?: (oldState: any, newState: any) => boolean; - context?: Context; - } + options: ConnectOption, ): Connector { if (!options) { options = {}; @@ -172,6 +175,13 @@ export function connect( return node; }; + if (options.forwardRef) { + const forwarded = forwardRef((props, ref) => { + return Wrapper({ ...props, ref: ref }); + }); + return forwarded as WrappedComponent; + } + return Wrapper; }; } From 3fb66526071fec18098ac2ac246fddf4e6adf80c Mon Sep 17 00:00:00 2001 From: huangxuan Date: Thu, 30 Nov 2023 16:09:33 +0800 Subject: [PATCH 062/108] =?UTF-8?q?[inulax]=20=E4=BF=AE=E5=A4=8D=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E4=BD=BF=E7=94=A8connect=20API=EF=BC=8Cprops=E9=87=8C?= =?UTF-8?q?=E6=B2=A1=E6=9C=89dispatch=E6=96=B9=E6=B3=95=EF=BC=9B=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E5=AF=B9=E8=B1=A1=E6=AF=94=E8=BE=83=E7=AE=97=E6=B3=95?= =?UTF-8?q?=EF=BC=8C=E9=98=B2=E6=AD=A2=E6=AD=BB=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/src/inulax/CommonUtils.ts | 54 +++++++++++++++---- .../inula/src/inulax/adapters/reduxReact.ts | 1 + 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/packages/inula/src/inulax/CommonUtils.ts b/packages/inula/src/inulax/CommonUtils.ts index 5202a200..ccbb182f 100644 --- a/packages/inula/src/inulax/CommonUtils.ts +++ b/packages/inula/src/inulax/CommonUtils.ts @@ -67,18 +67,50 @@ export function isPromise(obj: any): boolean { return isObject(obj) && typeof obj.then === 'function'; } -export function isSame(x, y) { - 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 isSame(x: unknown, y: unknown): boolean { + // 如果两个对象是同一个引用,直接返回true + if (x === y) { + return true; } + // 如果两个对象类型不同,直接返回false + if (typeof x !== typeof y) { + return false; + } + // 如果两个对象都是null或undefined,直接返回true + if (x == null || y == null) { + return true; + } + // 如果两个对象都是基本类型,比较他们的值是否相等 + if (typeof x !== 'object') { + return x === y; + } + // 如果两个对象都是数组,比较他们的长度是否相等,然后递归比较每个元素是否相等 + if (Array.isArray(x) && Array.isArray(y)) { + if (x.length !== y.length) { + return false; + } + for (let i = 0; i < x.length; i++) { + if (!isSame(x[i], y[i])) { + return false; + } + } + return true; + } + // 两个对象都是普通对象,首先比较他们的属性数量是否相等,然后递归比较每个属性的值是否相等 + if (typeof x === 'object' && typeof y === 'object') { + const keys1 = Object.keys(x!).sort(); + const keys2 = Object.keys(y!).sort(); + if (keys1.length !== keys2.length) { + return false; + } + for (let i = 0; i < keys1.length; i++) { + if (!isSame(x![keys1[i]], y![keys2[i]])) { + return false; + } + } + return true; + } + return false; } export function getDetailedType(val: any) { diff --git a/packages/inula/src/inulax/adapters/reduxReact.ts b/packages/inula/src/inulax/adapters/reduxReact.ts index 160dec37..1cc6cbb8 100644 --- a/packages/inula/src/inulax/adapters/reduxReact.ts +++ b/packages/inula/src/inulax/adapters/reduxReact.ts @@ -162,6 +162,7 @@ export function connect( mappedDispatch = mapDispatchToProps(store.dispatch, props); } } + mappedDispatch = Object.assign({}, mappedDispatch, { dispatch: store.dispatch }); const mergedProps = ( mergeProps || ((state, dispatch, originalProps) => { From 3019fa55811277a0d364408b2a24826cb1dd0d22 Mon Sep 17 00:00:00 2001 From: huangxuan Date: Thu, 30 Nov 2023 16:11:37 +0800 Subject: [PATCH 063/108] =?UTF-8?q?[inulax]=20=E4=BF=AE=E5=A4=8Dmiddleware?= =?UTF-8?q?(...)=20is=20not=20a=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/src/inulax/adapters/redux.ts | 45 +++++++++++-------- .../inula/src/inulax/adapters/reduxThunk.ts | 4 +- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/packages/inula/src/inulax/adapters/redux.ts b/packages/inula/src/inulax/adapters/redux.ts index b996abed..dbcb6924 100644 --- a/packages/inula/src/inulax/adapters/redux.ts +++ b/packages/inula/src/inulax/adapters/redux.ts @@ -27,12 +27,12 @@ export { createDispatchHook, } from './reduxReact'; -export type ReduxStoreHandler = { - reducer: (state: any, action: { type: string }) => any; - dispatch: (action: { type: string }) => void; - getState: () => any; - subscribe: (listener: () => void) => () => void; - replaceReducer: (reducer: (state: any, action: { type: string }) => any) => void; +export type ReduxStoreHandler = { + reducer(state: T, action: { type: string }): any; + dispatch(action: { type: string }): void; + getState(): T; + subscribe(listener: () => void): () => void; + replaceReducer(reducer: (state: T, action: { type: string }) => any): void; }; export type ReduxAction = { @@ -53,6 +53,9 @@ export type ReduxMiddleware = ( type Reducer = (state: any, action: ReduxAction) => any; +type StoreCreator = (reducer: Reducer, preloadedState?: any) => ReduxStoreHandler +type StoreEnhancer = (next: StoreCreator) => StoreCreator + function mergeData(state, data) { if (!data) { state.stateWrapper = data; @@ -87,7 +90,7 @@ function mergeData(state, data) { state.stateWrapper = data; } -export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): ReduxStoreHandler { +export function createStore(reducer: Reducer, preloadedState?: any, enhancers?: StoreEnhancer): ReduxStoreHandler { const store = createStoreX({ id: 'defaultStore', state: { stateWrapper: preloadedState }, @@ -150,19 +153,23 @@ export function combineReducers(reducers: { [key: string]: Reducer }): Reducer { }; } -function applyMiddlewares(store: ReduxStoreHandler, middlewares: ReduxMiddleware[]): void { - middlewares = middlewares.slice(); - middlewares.reverse(); - let dispatch = store.dispatch; - middlewares.forEach(middleware => { - dispatch = middleware(store)(dispatch); - }); - store.dispatch = dispatch; +function applyMiddlewares(createStore: StoreCreator, middlewares: ReduxMiddleware[]): StoreCreator { + return (reducer, preloadedState) => { + middlewares = middlewares.slice(); + middlewares.reverse(); + const storeObj = createStore(reducer, preloadedState); + let dispatch = storeObj.dispatch; + middlewares.forEach(middleware => { + dispatch = middleware(storeObj)(dispatch); + }); + storeObj.dispatch = dispatch; + return storeObj; + }; } -export function applyMiddleware(...middlewares: ReduxMiddleware[]): (store: ReduxStoreHandler) => void { - return store => { - return applyMiddlewares(store, middlewares); +export function applyMiddleware(...middlewares: ReduxMiddleware[]): (createStore: StoreCreator) => StoreCreator { + return createStore => { + return applyMiddlewares(createStore, middlewares); }; } @@ -170,7 +177,7 @@ type ActionCreator = (...params: any[]) => ReduxAction; type ActionCreators = { [key: string]: ActionCreator }; export type BoundActionCreator = (...params: any[]) => void; type BoundActionCreators = { [key: string]: BoundActionCreator }; -type Dispatch = (action) => any; +type Dispatch = (action: ReduxAction) => any; export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dispatch): BoundActionCreators { const boundActionCreators = {}; diff --git a/packages/inula/src/inulax/adapters/reduxThunk.ts b/packages/inula/src/inulax/adapters/reduxThunk.ts index cd14fee6..7eaaf227 100644 --- a/packages/inula/src/inulax/adapters/reduxThunk.ts +++ b/packages/inula/src/inulax/adapters/reduxThunk.ts @@ -35,5 +35,5 @@ function createThunkMiddleware(extraArgument?: any): ReduxMiddleware { } export const thunk = createThunkMiddleware(); -// @ts-ignore -thunk.withExtraArgument = createThunkMiddleware; + +export const withExtraArgument = createThunkMiddleware; From 0f87229318dc2439e0eb054f6c6c5fe52c217b93 Mon Sep 17 00:00:00 2001 From: huangxuan Date: Thu, 30 Nov 2023 16:13:51 +0800 Subject: [PATCH 064/108] =?UTF-8?q?[inulax]=20=E7=8A=B6=E6=80=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=99=A8dispatch=E6=96=B9=E6=B3=95=E5=BC=82=E6=AD=A5?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=97=B6=E6=95=B0=E6=8D=AE=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula/src/inulax/adapters/reduxReact.ts | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/inula/src/inulax/adapters/reduxReact.ts b/packages/inula/src/inulax/adapters/reduxReact.ts index 1cc6cbb8..4f3bd675 100644 --- a/packages/inula/src/inulax/adapters/reduxReact.ts +++ b/packages/inula/src/inulax/adapters/reduxReact.ts @@ -41,29 +41,27 @@ export function createStoreHook(context: Context): () => ReduxStoreHandler { }; } -export function createSelectorHook(context: Context): (selector?: (any) => any) => any { - const store = createStoreHook(context)() as unknown as ReduxStoreHandler; - return function (selector = state => state) { - const [b, fr] = useState(false); +export function createSelectorHook(context: Context): (selector?: ((state: unknown) => any) | undefined) => any { + const store = createStoreHook(context)(); + return function useSelector(selector = state => state) { + const [state, setState] = useState(() => store.getState()); useEffect(() => { - const unsubscribe = store.subscribe(() => fr(!b)); - return () => { - unsubscribe(); - }; - }); + const unsubscribe = store.subscribe(() => { + setState(store.getState()); + }); + return () => unsubscribe(); + }, []); - return selector(store.getState()); + return selector(state); }; } export function createDispatchHook(context: Context): () => BoundActionCreator { - const store = createStoreHook(context)() as unknown as ReduxStoreHandler; - return function () { - return action => { - store.dispatch(action); - }; - }.bind(store); + const store = createStoreHook(context)(); + return function useDispatch() { + return store.dispatch; + }; } export const useSelector = selector => { From e7dfd1b7e56557fc922c1135f056313f91872fd1 Mon Sep 17 00:00:00 2001 From: huangxuan Date: Thu, 30 Nov 2023 16:15:44 +0800 Subject: [PATCH 065/108] =?UTF-8?q?[inulax]=20=E4=BF=AE=E5=A4=8D=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=AE=A1=E7=90=86=E5=99=A8=E6=9B=B4=E6=96=B0=E5=AE=8C?= =?UTF-8?q?=E5=80=BC=E5=90=8E=EF=BC=8Cthis.props=E9=87=8C=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E4=B8=8D=E5=88=B0=E6=9C=80=E6=96=B0=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula/src/inulax/adapters/reduxReact.ts | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/packages/inula/src/inulax/adapters/reduxReact.ts b/packages/inula/src/inulax/adapters/reduxReact.ts index 4f3bd675..c404976f 100644 --- a/packages/inula/src/inulax/adapters/reduxReact.ts +++ b/packages/inula/src/inulax/adapters/reduxReact.ts @@ -111,41 +111,36 @@ export function connect( //this component should bear the type returned from mapping functions return (Component: OriginalComponent): WrappedComponent => { - const useStore = createStoreHook(options?.context || DefaultContext); + const useStore = createStoreHook(options.context || DefaultContext); //this component should mimic original type of component used const Wrapper: WrappedComponent = (props: OwnProps) => { - const [f, forceReload] = useState(true); + const store = useStore() as ReduxStoreHandler; + const [state, setState] = useState(() => store.getState()); - const store = useStore() as unknown as ReduxStoreHandler; useEffect(() => { - const unsubscribe = store.subscribe(() => forceReload(!f)); - return () => { - unsubscribe(); - }; + const unsubscribe = store.subscribe(() => { + setState(store.getState()); + }); + return () => unsubscribe(); + }, []); + + const previous = useRef<{ state: { [key: string]: any }, mappedState: StateProps }>({ + state: {}, + mappedState: {} as StateProps, }); - const previous = useRef({ - state: {}, - mappedState: {}, - }) as { - current: { - state: { [key: string]: any }; - mappedState: StateProps; - }; - }; - let mappedState: StateProps; - if (options?.areStatesEqual) { - if (options.areStatesEqual(previous.current.state, store.getState())) { + if (options.areStatesEqual) { + if (options.areStatesEqual(previous.current.state, state)) { mappedState = previous.current.mappedState as StateProps; } else { - mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps); + mappedState = mapStateToProps ? mapStateToProps(state, props) : ({} as StateProps); previous.current.mappedState = mappedState; } } else { - mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps); + mappedState = mapStateToProps ? mapStateToProps(state, props) : ({} as StateProps); previous.current.mappedState = mappedState; } let mappedDispatch: DispatchProps = {} as DispatchProps; @@ -154,6 +149,7 @@ export function connect( Object.entries(mapDispatchToProps).forEach(([key, value]) => { mappedDispatch[key] = (...args: ReduxAction[]) => { store.dispatch(value(...args)); + setState(store.getState()); }; }); } else { @@ -168,10 +164,9 @@ export function connect( }) )(mappedState, mappedDispatch, props); - previous.current.state = store.getState(); + previous.current.state = state; - const node = createElement(Component, mergedProps); - return node; + return createElement(Component, mergedProps); }; if (options.forwardRef) { From c2c794e0238df9654f3c556b61b2ac95aa718794 Mon Sep 17 00:00:00 2001 From: huangxuan Date: Thu, 30 Nov 2023 16:16:37 +0800 Subject: [PATCH 066/108] =?UTF-8?q?[inulax]=20=E4=BF=AE=E5=A4=8D=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=AE=A1=E7=90=86=E5=99=A8createStore=E4=BC=A0?= =?UTF-8?q?=E5=85=A5enhancer=E4=B8=8D=E7=94=9F=E6=95=88=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/src/inulax/adapters/redux.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/inula/src/inulax/adapters/redux.ts b/packages/inula/src/inulax/adapters/redux.ts index dbcb6924..c84b7eb3 100644 --- a/packages/inula/src/inulax/adapters/redux.ts +++ b/packages/inula/src/inulax/adapters/redux.ts @@ -133,12 +133,14 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?: dispatch: store.$a.dispatch, }; - enhancers && enhancers(result); - result.dispatch({ type: 'InulaX' }); store.reduxHandler = result; + if (typeof enhancers === 'function') { + return enhancers(createStore)(reducer, preloadedState); + } + return result as ReduxStoreHandler; } @@ -190,12 +192,12 @@ export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dis return boundActionCreators; } -export function compose(...middlewares: ReduxMiddleware[]) { - return (store: ReduxStoreHandler, extraArgument: any) => { - let val; - middlewares.reverse().forEach((middleware: ReduxMiddleware, index) => { +export function compose(...middlewares: ((...args: any[]) => any)[]): (...args: any[]) => T { + return (...args) => { + let val: any; + middlewares.reverse().forEach((middleware, index) => { if (!index) { - val = middleware(store, extraArgument); + val = middleware(...args); return; } val = middleware(val); From 3a30f6ce5972cfa3b1e8f1fe088f382394b4e3eb Mon Sep 17 00:00:00 2001 From: huangxuan Date: Thu, 30 Nov 2023 16:20:14 +0800 Subject: [PATCH 067/108] =?UTF-8?q?[UT]=20=E4=BF=AE=E6=94=B9UT=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E7=BB=93=E6=9E=9C,=20reduxAdapter=E4=B8=8Eredux?= =?UTF-8?q?=E4=BF=9D=E6=8C=81=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/inula/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx b/packages/inula/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx index 38b7445b..499be389 100644 --- a/packages/inula/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx +++ b/packages/inula/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx @@ -187,7 +187,7 @@ describe('Redux adapter', () => { reduxStore.dispatch({ type: 'toggle' }); reduxStore.dispatch({ type: 'toggle' }); - expect(counter).toBe(3); // NOTE: first action is always store initialization + expect(counter).toBe(2); // execute dispatch two times, applyMiddleware was called same times }); it('Should apply multiple enhancers', async () => { @@ -226,7 +226,7 @@ describe('Redux adapter', () => { reduxStore.dispatch({ type: 'toggle' }); - expect(counter).toBe(2); // NOTE: first action is always store initialization + expect(counter).toBe(1); // execute dispatch two times, applyMiddleware was called same times expect(lastAction).toBe('toggle'); expect(middlewareCallList[0]).toBe('callCounter'); expect(middlewareCallList[1]).toBe('lastFunctionStorage'); From 12fae0d2384f05c7d9bc6e8e8c5bae78e4525709 Mon Sep 17 00:00:00 2001 From: huangxuan Date: Thu, 30 Nov 2023 17:00:10 +0800 Subject: [PATCH 068/108] format --- packages/inula/src/inulax/adapters/redux.ts | 4 ++-- packages/inula/src/inulax/adapters/reduxReact.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/inula/src/inulax/adapters/redux.ts b/packages/inula/src/inulax/adapters/redux.ts index c84b7eb3..97ee92f1 100644 --- a/packages/inula/src/inulax/adapters/redux.ts +++ b/packages/inula/src/inulax/adapters/redux.ts @@ -53,8 +53,8 @@ export type ReduxMiddleware = ( type Reducer = (state: any, action: ReduxAction) => any; -type StoreCreator = (reducer: Reducer, preloadedState?: any) => ReduxStoreHandler -type StoreEnhancer = (next: StoreCreator) => StoreCreator +type StoreCreator = (reducer: Reducer, preloadedState?: any) => ReduxStoreHandler; +type StoreEnhancer = (next: StoreCreator) => StoreCreator; function mergeData(state, data) { if (!data) { diff --git a/packages/inula/src/inulax/adapters/reduxReact.ts b/packages/inula/src/inulax/adapters/reduxReact.ts index c404976f..0f74af0c 100644 --- a/packages/inula/src/inulax/adapters/reduxReact.ts +++ b/packages/inula/src/inulax/adapters/reduxReact.ts @@ -92,7 +92,7 @@ type Connector = (Component: OriginalComponent = { areStatesEqual?: (oldState: State, newState: State) => boolean; context?: Context; - forwardRef?: boolean + forwardRef?: boolean; } export function connect( From 4900251b2189fb24226bb690fd9dadda462b15cf Mon Sep 17 00:00:00 2001 From: huangxuan Date: Thu, 30 Nov 2023 17:26:32 +0800 Subject: [PATCH 069/108] =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/src/inulax/adapters/reduxReact.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/inula/src/inulax/adapters/reduxReact.ts b/packages/inula/src/inulax/adapters/reduxReact.ts index 0f74af0c..32eff8d0 100644 --- a/packages/inula/src/inulax/adapters/reduxReact.ts +++ b/packages/inula/src/inulax/adapters/reduxReact.ts @@ -103,7 +103,7 @@ export function connect( dispatchProps, ownProps ): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps } as unknown as MergedProps), - options: ConnectOption, + options?: ConnectOption ): Connector { if (!options) { options = {}; @@ -111,14 +111,13 @@ export function connect( //this component should bear the type returned from mapping functions return (Component: OriginalComponent): WrappedComponent => { - const useStore = createStoreHook(options.context || DefaultContext); + const useStore = createStoreHook(options?.context || DefaultContext); //this component should mimic original type of component used const Wrapper: WrappedComponent = (props: OwnProps) => { const store = useStore() as ReduxStoreHandler; const [state, setState] = useState(() => store.getState()); - useEffect(() => { const unsubscribe = store.subscribe(() => { setState(store.getState()); @@ -126,13 +125,13 @@ export function connect( return () => unsubscribe(); }, []); - const previous = useRef<{ state: { [key: string]: any }, mappedState: StateProps }>({ + const previous = useRef<{ state: { [key: string]: any }; mappedState: StateProps }>({ state: {}, mappedState: {} as StateProps, }); let mappedState: StateProps; - if (options.areStatesEqual) { + if (options?.areStatesEqual) { if (options.areStatesEqual(previous.current.state, state)) { mappedState = previous.current.mappedState as StateProps; } else { @@ -169,7 +168,7 @@ export function connect( return createElement(Component, mergedProps); }; - if (options.forwardRef) { + if (options?.forwardRef) { const forwarded = forwardRef((props, ref) => { return Wrapper({ ...props, ref: ref }); }); From 9dbd6244b5a5dd4c945c3251ee8406d39ec02603 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Fri, 1 Dec 2023 09:49:16 +0800 Subject: [PATCH 070/108] =?UTF-8?q?[inula]=20=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/inula/README.md b/packages/inula/README.md index 7a1082aa..d362820a 100644 --- a/packages/inula/README.md +++ b/packages/inula/README.md @@ -157,10 +157,6 @@ openinula团队会关注所有Pull Request,我们会review以及合入你的 1. `npm run build` 同时构建openinula UMD的prod版本和dev版本 2. `build-types` 单独构建openinula的类型提示@types目录 -#### 配套开发工具 - -- [openinula-devtool](https://www.XXXX.com): 可视化openinula项目页面的vDom树 - ## 开源许可协议 请查阅 License 获取开源许可协议的更多信息. From 15927bf37d91ddb968848580e24595f8a5168d46 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Fri, 1 Dec 2023 10:41:42 +0800 Subject: [PATCH 071/108] =?UTF-8?q?[inula]=20=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/src/renderer/hooks/UseReducerHook.ts | 2 +- packages/inula/src/renderer/vnode/VNodeCreator.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/inula/src/renderer/hooks/UseReducerHook.ts b/packages/inula/src/renderer/hooks/UseReducerHook.ts index fe56820b..e0ae2f3a 100644 --- a/packages/inula/src/renderer/hooks/UseReducerHook.ts +++ b/packages/inula/src/renderer/hooks/UseReducerHook.ts @@ -21,7 +21,7 @@ import { setStateChange } from '../render/FunctionComponent'; import { getHookStage, HookStage } from './HookStage'; import type { VNode } from '../Types'; import { getProcessingVNode } from '../GlobalVar'; -import { markUpdatedInRender } from "./HookMain"; +import { markUpdatedInRender } from './HookMain'; // 构造新的Update数组 function insertUpdate(action: A, hook: Hook): Update { diff --git a/packages/inula/src/renderer/vnode/VNodeCreator.ts b/packages/inula/src/renderer/vnode/VNodeCreator.ts index 142bac33..55ee5247 100644 --- a/packages/inula/src/renderer/vnode/VNodeCreator.ts +++ b/packages/inula/src/renderer/vnode/VNodeCreator.ts @@ -74,7 +74,7 @@ export function getLazyVNodeTag(lazyComp: any): string { } else if (lazyComp !== undefined && lazyComp !== null && typeLazyMap[lazyComp.vtype]) { return typeLazyMap[lazyComp.vtype]; } - throw Error("Inula can't resolve the content of lazy"); + throw Error('Inula can\'t resolve the content of lazy'); } // 创建processing From a34c7d6c02ef9ec592466ba2317ef64c36b835e6 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Fri, 1 Dec 2023 10:42:32 +0800 Subject: [PATCH 072/108] =?UTF-8?q?[inula-request]=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20useIR=20=E4=BE=9D=E8=B5=96=E5=A4=9A=E4=BB=BD=20open?= =?UTF-8?q?inula=20=E6=8A=A5=E9=94=99=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-request/rollup.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/inula-request/rollup.config.js b/packages/inula-request/rollup.config.js index 54689cb2..439ea2ab 100644 --- a/packages/inula-request/rollup.config.js +++ b/packages/inula-request/rollup.config.js @@ -41,4 +41,7 @@ export default { presets: ['@babel/preset-env'] }) ], + external:[ + 'openinula' + ], }; From 43f96b74e13df1ed8302ba47a11fbef8795b4eb4 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Fri, 1 Dec 2023 10:44:14 +0800 Subject: [PATCH 073/108] =?UTF-8?q?[inula-request]=20=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-request/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/inula-request/package.json b/packages/inula-request/package.json index 1a8fbe97..b2230d30 100644 --- a/packages/inula-request/package.json +++ b/packages/inula-request/package.json @@ -1,6 +1,6 @@ { "name": "inula-request", - "version": "0.0.5", + "version": "0.0.7", "description": "Inula-request brings you a convenient request experience!", "main": "./dist/inulaRequest.js", "scripts": { From 503577d4c10cfd97b34568fffa9f545d16fd9213 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Fri, 1 Dec 2023 10:44:35 +0800 Subject: [PATCH 074/108] =?UTF-8?q?[inula-intl]=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-intl/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/inula-intl/package.json b/packages/inula-intl/package.json index 00cca8ea..910ca492 100644 --- a/packages/inula-intl/package.json +++ b/packages/inula-intl/package.json @@ -1,6 +1,6 @@ { "name": "inula-intl", - "version": "0.0.2", + "version": "0.0.3", "description": "", "main": "build/intl.umd.js", "type": "commonjs", From 3c991461934aab7dba39b56b5ca81f0135fd0d51 Mon Sep 17 00:00:00 2001 From: c00364821 Date: Fri, 1 Dec 2023 10:44:38 +0800 Subject: [PATCH 075/108] =?UTF-8?q?=E3=80=90core=E3=80=91portal=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/src/event/EventBinding.ts | 9 ++++++++- packages/inula/src/event/EventHub.ts | 7 +++++++ packages/inula/src/renderer/render/DomPortal.ts | 3 ++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/inula/src/event/EventBinding.ts b/packages/inula/src/event/EventBinding.ts index 01b3ea54..f3e9f58e 100644 --- a/packages/inula/src/event/EventBinding.ts +++ b/packages/inula/src/event/EventBinding.ts @@ -16,7 +16,7 @@ /** * 事件绑定实现,分为绑定委托事件和非委托事件 */ -import { allDelegatedInulaEvents, simulatedDelegatedEvents } from './EventHub'; +import { allDelegatedInulaEvents, portalDefaultDelegatedEvents, simulatedDelegatedEvents } from './EventHub'; import { isDocument } from '../dom/utils/Common'; import { getNearestVNode, getNonDelegatedListenerMap } from '../dom/DOMInternalKeys'; import { asyncUpdates, runDiscreteUpdates } from '../renderer/TreeBuilder'; @@ -89,6 +89,13 @@ export function listenSimulatedDelegatedEvents(root: VNode) { } } +// portal绑定默认事件 +export function listenPortalEvents(root: VNode) { + for (let i = 0; i < portalDefaultDelegatedEvents.length; i++) { + lazyDelegateOnRoot(root, portalDefaultDelegatedEvents[i]); + } +} + // 通过inula事件名获取到native事件名 function getNativeEvtName(inulaEventName, capture) { let nativeName; diff --git a/packages/inula/src/event/EventHub.ts b/packages/inula/src/event/EventHub.ts index 2f8e8a1f..0dc01c50 100644 --- a/packages/inula/src/event/EventHub.ts +++ b/packages/inula/src/event/EventHub.ts @@ -16,6 +16,13 @@ // 需要委托的inula事件和原生事件对应关系 export const allDelegatedInulaEvents = new Map(); +/** + * Portal根节点默认绑定事件,解决常见事件无法冒泡到parent vnode的问题 + * 例如:parent vNode节点绑定了mousedown事件,子节点为portal节点,子节点下元素未绑定mousedown事件 + * 此时,点击portal下子元素,mousedown事件无法冒泡到parentNode + */ +export const portalDefaultDelegatedEvents = ['onMouseDown', 'onMouseUp', 'onKeyDown', 'onKeyUp', 'onFocus', 'onBlur', 'onClick']; + // 模拟委托事件,不冒泡事件需要利用其他事件来触发冒泡过程 export const simulatedDelegatedEvents = ['onMouseEnter', 'onMouseLeave']; // 所有委托的原生事件集合 diff --git a/packages/inula/src/renderer/render/DomPortal.ts b/packages/inula/src/renderer/render/DomPortal.ts index 27bad601..24628605 100644 --- a/packages/inula/src/renderer/render/DomPortal.ts +++ b/packages/inula/src/renderer/render/DomPortal.ts @@ -17,11 +17,12 @@ import type { VNode } from '../Types'; import { resetNamespaceCtx, setNamespaceCtx } from '../ContextSaver'; import { createChildrenByDiff } from '../diff/nodeDiffComparator'; import { popCurrentRoot, pushCurrentRoot } from '../RootStack'; -import { listenSimulatedDelegatedEvents } from '../../event/EventBinding'; +import { listenPortalEvents, listenSimulatedDelegatedEvents } from '../../event/EventBinding'; export function bubbleRender(processing: VNode) { resetNamespaceCtx(processing); listenSimulatedDelegatedEvents(processing); + listenPortalEvents(processing); popCurrentRoot(); } From 8274064ae69c28875cb6e7235a2481ddd5f2b987 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Fri, 1 Dec 2023 10:44:55 +0800 Subject: [PATCH 076/108] [all] update --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 717a5b85..cda89608 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "openinula", + "name": "inula", "description": "OpenInula is a JavaScript framework library.", "version": "0.0.1", "private": true, From ca3addf9fe9c65f6c6d215d1f694c46e5191535a Mon Sep 17 00:00:00 2001 From: c00364821 Date: Fri, 1 Dec 2023 11:28:50 +0800 Subject: [PATCH 077/108] =?UTF-8?q?=E8=A7=A3=E5=86=B3portal=20root?= =?UTF-8?q?=E5=85=A5=E6=A0=88=E9=A1=BA=E5=BA=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ComponentTest/PortalComponent.test.js | 94 +++++++++++++++++++ packages/inula/src/renderer/TreeBuilder.ts | 7 +- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/packages/inula/scripts/__tests__/ComponentTest/PortalComponent.test.js b/packages/inula/scripts/__tests__/ComponentTest/PortalComponent.test.js index 4c6924b9..98e55340 100644 --- a/packages/inula/scripts/__tests__/ComponentTest/PortalComponent.test.js +++ b/packages/inula/scripts/__tests__/ComponentTest/PortalComponent.test.js @@ -286,4 +286,98 @@ describe('PortalComponent Test', () => { dispatchChangeEvent(inputRef.current, 'test'); expect(fn).toHaveBeenCalledTimes(1); }); + + + it('portal场景下,portal下元素点击事件冒泡到父元素', () => { + class Dialog extends Inula.Component { + node; + + constructor(props) { + super(props); + this.node = window.document.createElement('div'); + window.document.body.appendChild(this.node); + } + + render() { + return Inula.createPortal(this.props.children, this.node); + } + } + + const fn = jest.fn(); + const subRef = Inula.createRef(); + + function App() { + return ( +
+ +
+
+
+ ); + } + + Inula.render(, container); + Inula.act(() => { + subRef.current.dispatchEvent(new Event('click', { bubbles: true })); + }); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('portal嵌套场景下事件委托', () => { + class Dialog extends Inula.Component { + node; + + constructor(props) { + super(props); + this.node = window.document.createElement('div'); + window.document.body.appendChild(this.node); + } + + render() { + return Inula.createPortal(this.props.children, this.node); + } + } + + const fn = jest.fn(); + const inputRef = Inula.createRef(); + let value = ''; + const onChange = (evt) => { + value = evt.target.value; + } + + let showSubPortal = () => {}; + + function App() { + return ( +
+ + + + +
+ ); + } + + function Sub() { + const [show, setShow] = Inula.useState(false); + showSubPortal = setShow; + return ( +
+ { + show && + + + + } +
+ ) + } + + Inula.render(, container); + Inula.act(() => { + showSubPortal(true); + }); + dispatchChangeEvent(inputRef.current, 'test'); + expect(value).toEqual('test'); + }); }); diff --git a/packages/inula/src/renderer/TreeBuilder.ts b/packages/inula/src/renderer/TreeBuilder.ts index 50326e25..c29126fb 100644 --- a/packages/inula/src/renderer/TreeBuilder.ts +++ b/packages/inula/src/renderer/TreeBuilder.ts @@ -244,17 +244,22 @@ export function calcStartUpdateVNode(treeRoot: VNode) { // 在局部更新时,从上到下恢复父节点的context和PortalStack function recoverTreeContext(vNode: VNode) { const contextProviders: VNode[] = []; + const portalRoots: VNode[] = []; let parent = vNode.parent; while (parent !== null) { if (parent.tag === ContextProvider) { contextProviders.unshift(parent); + } else if (parent.tag === DomPortal) { + portalRoots.unshift(parent); } if (parent.tag === DomPortal) { pushCurrentRoot(parent); } parent = parent.parent; } - + portalRoots.forEach(node => { + pushCurrentRoot(node); + }); contextProviders.forEach(node => { setContext(node, node.props.value); }); From cfd55220f8a42f14cf3055b978ce0d9a48892d53 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Fri, 1 Dec 2023 12:15:24 +0800 Subject: [PATCH 078/108] =?UTF-8?q?[create-inula]=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=93=8D=E5=BA=94=E5=BC=8F=20API=20=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../generators/Simple-reactive-app/index.js | 43 ++++++++++ .../generators/Simple-reactive-app/meta.json | 3 + .../templates/vite/index.html | 11 +++ .../templates/vite/package.json | 25 ++++++ .../templates/vite/src/ReactiveComponent.jsx | 38 +++++++++ .../templates/vite/src/index.css | 57 +++++++++++++ .../templates/vite/src/index.jsx | 45 ++++++++++ .../templates/vite/vite.config.js | 29 +++++++ .../templates/webpack/package.json | 29 +++++++ .../templates/webpack/src/App.js | 47 +++++++++++ .../webpack/src/ReactiveComponent.js | 38 +++++++++ .../templates/webpack/src/index.html | 11 +++ .../templates/webpack/src/index.js | 19 +++++ .../templates/webpack/src/styles.css | 57 +++++++++++++ .../templates/webpack/webpack.config.js | 84 +++++++++++++++++++ packages/create-inula/package.json | 2 +- 16 files changed, 537 insertions(+), 1 deletion(-) create mode 100644 packages/create-inula/lib/generators/Simple-reactive-app/index.js create mode 100644 packages/create-inula/lib/generators/Simple-reactive-app/meta.json create mode 100644 packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/index.html create mode 100644 packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/package.json create mode 100644 packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/src/ReactiveComponent.jsx create mode 100644 packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/src/index.css create mode 100644 packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/src/index.jsx create mode 100644 packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/vite.config.js create mode 100644 packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/package.json create mode 100644 packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/App.js create mode 100644 packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/ReactiveComponent.js create mode 100644 packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.html create mode 100644 packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.js create mode 100644 packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/styles.css create mode 100644 packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/webpack.config.js diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/index.js b/packages/create-inula/lib/generators/Simple-reactive-app/index.js new file mode 100644 index 00000000..2d52bc4b --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-reactive-app/index.js @@ -0,0 +1,43 @@ +/* + * 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 BasicGenerator = require('../../BasicGenerator'); + +class Generator extends BasicGenerator { + prompting() { + return this.prompt([ + { + type: 'list', + name: 'bundlerType', + message: 'Please select the build type', + choices: ['webpack', 'vite'], + }, + ]).then(props => { + this.prompts = props; + }); + } + + writing() { + const src = this.templatePath(this.prompts.bundlerType); + const dest = this.destinationPath(); + this.writeFiles(src, dest, { + context: { + ...this.prompts, + }, + }); + } +} + +module.exports = Generator; diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/meta.json b/packages/create-inula/lib/generators/Simple-reactive-app/meta.json new file mode 100644 index 00000000..8f8f9e66 --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-reactive-app/meta.json @@ -0,0 +1,3 @@ +{ + "description": "simple reactive app template." +} diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/index.html b/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/index.html new file mode 100644 index 00000000..9f37946a --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/index.html @@ -0,0 +1,11 @@ + + + + + My Inula App + + +
+ + + diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/package.json b/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/package.json new file mode 100644 index 00000000..f74bdda6 --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/package.json @@ -0,0 +1,25 @@ +{ + "name": "inula-vite-app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "vite", + "build": "vite build" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "openinula": "0.0.0-experimental-20231201" + }, + "devDependencies": { + "@babel/core": "^7.21.4", + "@babel/preset-env": "^7.21.4", + "@babel/preset-react": "^7.18.6", + "@vitejs/plugin-react": "^3.1.0", + "@vitejs/plugin-react-refresh": "^1.3.6", + "babel-plugin-import": "^1.13.6", + "vite": "^4.2.1" + } +} diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/src/ReactiveComponent.jsx b/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/src/ReactiveComponent.jsx new file mode 100644 index 00000000..cca83f05 --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/src/ReactiveComponent.jsx @@ -0,0 +1,38 @@ +/* + * 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 Inula, { useComputed, useReactive, useRef } from 'openinula'; + +function ReactiveComponent() { + const renderCount = ++useRef(0).current; + + const data = useReactive({ count: 0 }); + const countText = useComputed(() => { + return `计时: ${data.count.get()}`; + }); + + setInterval(() => { + data.count.set(c => c + 1); + }, 1000); + + return ( +
+
{countText}
+
组件渲染次数:{renderCount}
+
+ ); +} + +export default ReactiveComponent; diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/src/index.css b/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/src/index.css new file mode 100644 index 00000000..f08fdb8a --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/src/index.css @@ -0,0 +1,57 @@ +* { + box-sizing: border-box; +} + +body, +html { + margin: 0; + padding: 0; + font-family: 'Montserrat', sans-serif; + line-height: 1.6; + color: #fff; + background: linear-gradient(120deg, #6a11cb 0%, #2575fc 100%); + height: 100vh; + display: flex; + align-items: center; + justify-content: center; +} + +.container { + text-align: center; +} + +.hero-title { + font-size: 3em; + margin-bottom: 20px; +} + +.hero-subtitle { + font-size: 1.5em; + margin-bottom: 50px; +} + +.content { + display: flex; + justify-content: space-between; + align-items: center; + margin: 1vh; +} + +.card { + background: rgba(255, 255, 255, 0.1); + border-radius: 5px; + padding: 20px; + width: 100%; + box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(4px); +} + +.card h2, +.card p { + color: #fff; +} + +.card a { + color: #fff; + text-decoration: underline; +} diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/src/index.jsx b/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/src/index.jsx new file mode 100644 index 00000000..42179269 --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/src/index.jsx @@ -0,0 +1,45 @@ +/* + * 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 Inula from 'openinula'; +import ReactiveComponent from './ReactiveComponent'; +import './index.css'; + +function App() { + return ( +
+
+

欢迎来到 Inula 项目!

+

你已成功创建你的第一个响应式 Inula 项目

+
+
+
+ +
+
+
+
+

了解更多

+

+ 要了解 Inula,查看{' '} + Inula 官网 +

+
+
+
+ ); +} + +Inula.render(, document.getElementById('root')); diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/vite.config.js b/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/vite.config.js new file mode 100644 index 00000000..43065ece --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/vite.config.js @@ -0,0 +1,29 @@ +/* + * 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 react from '@vitejs/plugin-react'; + +let alias = { + react: 'openinula', // 新增 + 'react-dom': 'openinula', // 新增 + 'react/jsx-dev-runtime': 'openinula/jsx-dev-runtime', +}; + +export default { + plugins: [react()], + resolve: { + alias, + }, +}; diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/package.json b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/package.json new file mode 100644 index 00000000..783f626a --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/package.json @@ -0,0 +1,29 @@ +{ + "name": "inula-webpack-app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "webpack serve --mode development", + "build": "webpack --mode production" + }, + "author": "", + "license": "ISC", + "dependencies": { + "openinula": "0.0.0-experimental-20231201" + }, + "devDependencies": { + "@babel/core": "^7.21.4", + "@babel/preset-env": "^7.21.4", + "@babel/preset-react": "^7.18.6", + "babel-loader": "^9.1.2", + "css-loader": "^6.7.3", + "file-loader": "^6.2.0", + "html-webpack-plugin": "^5.5.0", + "style-loader": "^3.3.2", + "url-loader": "^4.1.1", + "webpack": "^5.77.0", + "webpack-cli": "^5.0.1", + "webpack-dev-server": "^4.13.2" + } +} diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/App.js b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/App.js new file mode 100644 index 00000000..5a294011 --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/App.js @@ -0,0 +1,47 @@ +/* + * 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 Inula from 'openinula'; +import ReactiveComponent from './ReactiveComponent'; +import './styles.css'; + +class App extends Inula.Component { + render() { + return ( +
+
+

欢迎来到 Inula 项目!

+

你已成功创建你的第一个响应式 Inula 项目

+
+
+
+ +
+
+
+
+

了解更多

+

+ 要了解 Inula,查看{' '} + Inula 官网 +

+
+
+
+ ); + } +} + +export default App; diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/ReactiveComponent.js b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/ReactiveComponent.js new file mode 100644 index 00000000..cca83f05 --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/ReactiveComponent.js @@ -0,0 +1,38 @@ +/* + * 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 Inula, { useComputed, useReactive, useRef } from 'openinula'; + +function ReactiveComponent() { + const renderCount = ++useRef(0).current; + + const data = useReactive({ count: 0 }); + const countText = useComputed(() => { + return `计时: ${data.count.get()}`; + }); + + setInterval(() => { + data.count.set(c => c + 1); + }, 1000); + + return ( +
+
{countText}
+
组件渲染次数:{renderCount}
+
+ ); +} + +export default ReactiveComponent; diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.html b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.html new file mode 100644 index 00000000..c966e725 --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.html @@ -0,0 +1,11 @@ + + + + + Inula App + + +
+ + + diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.js b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.js new file mode 100644 index 00000000..c384f05a --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.js @@ -0,0 +1,19 @@ +/* + * 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 Inula from 'openinula'; +import App from './App'; + +Inula.render(, document.getElementById('root')); diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/styles.css b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/styles.css new file mode 100644 index 00000000..f08fdb8a --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/styles.css @@ -0,0 +1,57 @@ +* { + box-sizing: border-box; +} + +body, +html { + margin: 0; + padding: 0; + font-family: 'Montserrat', sans-serif; + line-height: 1.6; + color: #fff; + background: linear-gradient(120deg, #6a11cb 0%, #2575fc 100%); + height: 100vh; + display: flex; + align-items: center; + justify-content: center; +} + +.container { + text-align: center; +} + +.hero-title { + font-size: 3em; + margin-bottom: 20px; +} + +.hero-subtitle { + font-size: 1.5em; + margin-bottom: 50px; +} + +.content { + display: flex; + justify-content: space-between; + align-items: center; + margin: 1vh; +} + +.card { + background: rgba(255, 255, 255, 0.1); + border-radius: 5px; + padding: 20px; + width: 100%; + box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(4px); +} + +.card h2, +.card p { + color: #fff; +} + +.card a { + color: #fff; + text-decoration: underline; +} diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/webpack.config.js b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/webpack.config.js new file mode 100644 index 00000000..9056a484 --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/webpack.config.js @@ -0,0 +1,84 @@ +/* + * 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 path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +module.exports = { + entry: './src/index.js', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'bundle.js', + }, + module: { + rules: [ + { + test: /\.(js|jsx)$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + presets: [ + '@babel/preset-env', + [ + '@babel/preset-react', + { + runtime: 'automatic', // 新增 + importSource: 'openinula', // 新增 + }, + ], + ], + }, + }, + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + { + test: /\.(png|jpe?g|gif)$/i, + use: [ + { + loader: 'file-loader', + options: { + name: '[name].[ext]', + outputPath: 'images/', + publicPath: 'images/', + }, + }, + ], + }, + { + test: /\.(woff|woff2|eot|ttf|otf)$/, + use: ['file-loader'], + }, + ], + }, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(__dirname, 'src/index.html'), + filename: 'index.html', + }), + ], + devServer: { + static: path.join(__dirname, 'dist'), + compress: true, + port: 9000, + open: true, + }, + resolve: { + extensions: ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'], + }, +}; diff --git a/packages/create-inula/package.json b/packages/create-inula/package.json index 27f2bbee..6dabc8c0 100644 --- a/packages/create-inula/package.json +++ b/packages/create-inula/package.json @@ -1,6 +1,6 @@ { "name": "create-inula", - "version": "0.0.3", + "version": "0.0.5", "description": "", "main": "index.js", "bin": { From 45ebc9b97d72ecca14407f26a5eef696d5413bde Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Fri, 1 Dec 2023 14:40:05 +0800 Subject: [PATCH 079/108] =?UTF-8?q?[create-inula]=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=93=8D=E5=BA=94=E5=BC=8F=20API=20=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Simple-app/templates/webpack/src/{App.js => App.jsx} | 0 .../Simple-app/templates/webpack/src/{index.js => index.jsx} | 0 .../generators/Simple-app/templates/webpack/webpack.config.js | 2 +- .../templates/webpack/src/{App.js => App.jsx} | 0 .../src/{ReactiveComponent.js => ReactiveComponent.jsx} | 0 .../templates/webpack/src/{index.js => index.jsx} | 0 .../Simple-reactive-app/templates/webpack/webpack.config.js | 2 +- packages/create-inula/package.json | 2 +- packages/inula/scripts/rollup/rollup.config.js | 4 ++-- 9 files changed, 5 insertions(+), 5 deletions(-) rename packages/create-inula/lib/generators/Simple-app/templates/webpack/src/{App.js => App.jsx} (100%) rename packages/create-inula/lib/generators/Simple-app/templates/webpack/src/{index.js => index.jsx} (100%) rename packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/{App.js => App.jsx} (100%) rename packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/{ReactiveComponent.js => ReactiveComponent.jsx} (100%) rename packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/{index.js => index.jsx} (100%) diff --git a/packages/create-inula/lib/generators/Simple-app/templates/webpack/src/App.js b/packages/create-inula/lib/generators/Simple-app/templates/webpack/src/App.jsx similarity index 100% rename from packages/create-inula/lib/generators/Simple-app/templates/webpack/src/App.js rename to packages/create-inula/lib/generators/Simple-app/templates/webpack/src/App.jsx diff --git a/packages/create-inula/lib/generators/Simple-app/templates/webpack/src/index.js b/packages/create-inula/lib/generators/Simple-app/templates/webpack/src/index.jsx similarity index 100% rename from packages/create-inula/lib/generators/Simple-app/templates/webpack/src/index.js rename to packages/create-inula/lib/generators/Simple-app/templates/webpack/src/index.jsx diff --git a/packages/create-inula/lib/generators/Simple-app/templates/webpack/webpack.config.js b/packages/create-inula/lib/generators/Simple-app/templates/webpack/webpack.config.js index 9056a484..8b46f7a5 100644 --- a/packages/create-inula/lib/generators/Simple-app/templates/webpack/webpack.config.js +++ b/packages/create-inula/lib/generators/Simple-app/templates/webpack/webpack.config.js @@ -17,7 +17,7 @@ const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { - entry: './src/index.js', + entry: './src/index.jsx', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js', diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/App.js b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/App.jsx similarity index 100% rename from packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/App.js rename to packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/App.jsx diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/ReactiveComponent.js b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/ReactiveComponent.jsx similarity index 100% rename from packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/ReactiveComponent.js rename to packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/ReactiveComponent.jsx diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.js b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.jsx similarity index 100% rename from packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.js rename to packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.jsx diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/webpack.config.js b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/webpack.config.js index 9056a484..8b46f7a5 100644 --- a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/webpack.config.js +++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/webpack.config.js @@ -17,7 +17,7 @@ const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { - entry: './src/index.js', + entry: './src/index.jsx', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js', diff --git a/packages/create-inula/package.json b/packages/create-inula/package.json index 6dabc8c0..c10c8de8 100644 --- a/packages/create-inula/package.json +++ b/packages/create-inula/package.json @@ -1,6 +1,6 @@ { "name": "create-inula", - "version": "0.0.5", + "version": "0.0.6", "description": "", "main": "index.js", "bin": { diff --git a/packages/inula/scripts/rollup/rollup.config.js b/packages/inula/scripts/rollup/rollup.config.js index 45ee6f67..74f90045 100644 --- a/packages/inula/scripts/rollup/rollup.config.js +++ b/packages/inula/scripts/rollup/rollup.config.js @@ -93,8 +93,8 @@ function genConfig(mode) { mode === 'production' && terser(), copy([ { - from: path.join(libDir, '/npm/index.js'), - to: path.join(outDir, 'index.js'), + from: path.join(libDir, '/npm/index.jsx'), + to: path.join(outDir, 'index.jsx'), } ]), ], From eda41a98a67049a7775610fabdaebd51fb557c00 Mon Sep 17 00:00:00 2001 From: HoikanChen Date: Fri, 1 Dec 2023 15:16:49 +0800 Subject: [PATCH 080/108] =?UTF-8?q?[create-inula]=E6=98=8E=E7=A1=AEno?= =?UTF-8?q?de=E7=89=88=E6=9C=AC=20&=20=E9=A1=B9=E7=9B=AE=E5=90=8D=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../generators/Simple-app/templates/vite/README.md | 4 ++++ .../Simple-reactive-app/templates/vite/README.md | 4 ++++ packages/create-inula/lib/run.js | 12 +++++++++++- packages/create-inula/package.json | 3 +++ 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 packages/create-inula/lib/generators/Simple-app/templates/vite/README.md create mode 100644 packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/README.md diff --git a/packages/create-inula/lib/generators/Simple-app/templates/vite/README.md b/packages/create-inula/lib/generators/Simple-app/templates/vite/README.md new file mode 100644 index 00000000..542c363a --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-app/templates/vite/README.md @@ -0,0 +1,4 @@ +# openinula + vite + +该模板提供了 `openinula` 工作在 `vite`的基础配置。 +> 请注意由于Vite插件有node版本限制,请使用`node -v`命令确认node版本大于等于node v18。 \ No newline at end of file diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/README.md b/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/README.md new file mode 100644 index 00000000..542c363a --- /dev/null +++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/README.md @@ -0,0 +1,4 @@ +# openinula + vite + +该模板提供了 `openinula` 工作在 `vite`的基础配置。 +> 请注意由于Vite插件有node版本限制,请使用`node -v`命令确认node版本大于等于node v18。 \ No newline at end of file diff --git a/packages/create-inula/lib/run.js b/packages/create-inula/lib/run.js index 15099589..268022ed 100644 --- a/packages/create-inula/lib/run.js +++ b/packages/create-inula/lib/run.js @@ -62,7 +62,17 @@ const run = async config => { } process.emit('message', { type: 'prompt' }); - let { type } = config; + let { type, name } = config; + if (!name) { + const answers = await inquirer.prompt([ + { + name: 'projectName', + message: 'Project name', + type: 'input' + }, + ]); + config.name = answers.projectName; + } if (!type) { const answers = await inquirer.prompt([ { diff --git a/packages/create-inula/package.json b/packages/create-inula/package.json index c10c8de8..88eac59c 100644 --- a/packages/create-inula/package.json +++ b/packages/create-inula/package.json @@ -6,6 +6,9 @@ "bin": { "create-inula": "bin/cli.js" }, + "engines": { + "node": ">= 18.0.0" + }, "files": [ "bin", "lib", From 761cb31701f490b874b085053db47f663eec05f6 Mon Sep 17 00:00:00 2001 From: huangxuan Date: Fri, 1 Dec 2023 15:42:00 +0800 Subject: [PATCH 081/108] update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a2fe80bb..7b99c861 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ### 核心能力 -#### 响应式API +#### 响应式API(实验性功能,可在reactive分支查看代码或使用npm仓中experiment版本体验) * openInula 通过最小化重新渲染的范围,从而进行高效的UI渲染。这种方式避免了虚拟 DOM 的开销,使得 openInula 在性能方面表现出色。 * openInula 通过比较变化前后的 JavaScript 对象以细粒度的依赖追踪机制来实现响应式更新,无需用户过度关注性能优化。 From 1af8f0271abdfc96fb0584479d39583f7163e004 Mon Sep 17 00:00:00 2001 From: huangxuan Date: Fri, 1 Dec 2023 15:42:00 +0800 Subject: [PATCH 082/108] update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a2fe80bb..7b99c861 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ### 核心能力 -#### 响应式API +#### 响应式API(实验性功能,可在reactive分支查看代码或使用npm仓中experiment版本体验) * openInula 通过最小化重新渲染的范围,从而进行高效的UI渲染。这种方式避免了虚拟 DOM 的开销,使得 openInula 在性能方面表现出色。 * openInula 通过比较变化前后的 JavaScript 对象以细粒度的依赖追踪机制来实现响应式更新,无需用户过度关注性能优化。 From 1f0dbaa1803ac728b6839694606fc0d477decc0b Mon Sep 17 00:00:00 2001 From: huangxuan Date: Fri, 1 Dec 2023 15:43:36 +0800 Subject: [PATCH 083/108] =?UTF-8?q?inula-router=E9=97=AE=E9=A2=98=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=8F=8A=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/connect-router/connectedRouter.tsx | 9 +++++-- .../inula-router/src/history/hashHistory.ts | 4 +-- packages/inula-router/src/history/utils.ts | 14 +++++----- packages/inula-router/src/router/NavLink.tsx | 27 ++++++++++++++----- packages/inula-router/src/router/Route.tsx | 10 ++++--- packages/inula-router/src/router/Router.tsx | 16 +++++++---- .../inula-router/src/router/matcher/parser.ts | 2 +- .../inula-router/src/router/withRouter.tsx | 3 ++- 8 files changed, 58 insertions(+), 27 deletions(-) diff --git a/packages/inula-router/src/connect-router/connectedRouter.tsx b/packages/inula-router/src/connect-router/connectedRouter.tsx index a526663a..8ce5a383 100644 --- a/packages/inula-router/src/connect-router/connectedRouter.tsx +++ b/packages/inula-router/src/connect-router/connectedRouter.tsx @@ -16,7 +16,7 @@ import Inula from 'openinula'; import { useLayoutEffect, useRef, reduxAdapter, InulaNode } from 'openinula'; import { connect, ReactReduxContext } from 'react-redux'; -import { Store } from 'redux'; +import type { Store } from 'redux'; import { History, Location, Router } from '../router'; import { Action, DefaultStateType, Navigation } from '../history/types'; import { ActionMessage, onLocationChanged } from './actions'; @@ -130,9 +130,14 @@ function getConnectedRouter(type: StoreType) { ); }; + const ConnectedHRouterWithContext = (props: any) => { + const { store, ...rest } = props; + return ; + }; + // 针对不同的Store类型,使用对应的connect函数 if (type === 'InulaXCompat') { - return hConnect(null as any, mapDispatchToProps)(ConnectedRouterWithContext as any); + return hConnect(null as any, mapDispatchToProps)(ConnectedHRouterWithContext as any); } if (type === 'Redux') { return connect(null, mapDispatchToProps)(ConnectedRouterWithContext); diff --git a/packages/inula-router/src/history/hashHistory.ts b/packages/inula-router/src/history/hashHistory.ts index c44a85bb..f1cf421d 100644 --- a/packages/inula-router/src/history/hashHistory.ts +++ b/packages/inula-router/src/history/hashHistory.ts @@ -107,7 +107,7 @@ export function createHashHistory(option: HashHistoryOptio warning(state !== undefined, 'Hash history does not support state, it will be ignored'); const action = Action.push; - const location = createLocation(history.location, to, undefined, ''); + const location = createLocation(history.location, to, state, ''); transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => { if (!isJump) { @@ -132,7 +132,7 @@ export function createHashHistory(option: HashHistoryOptio function replace(to: To, state?: S) { warning(state !== undefined, 'Hash history does not support state, it will be ignored'); const action = Action.replace; - const location = createLocation(history.location, to, undefined, ''); + const location = createLocation(history.location, to, state, ''); transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => { if (!isJump) { diff --git a/packages/inula-router/src/history/utils.ts b/packages/inula-router/src/history/utils.ts index 27447967..8a671372 100644 --- a/packages/inula-router/src/history/utils.ts +++ b/packages/inula-router/src/history/utils.ts @@ -28,8 +28,8 @@ export function createPath(path: Partial): string { } export function parsePath(url: string): Partial { + let pathname = url || '/'; const parsedPath: Partial = { - pathname: url || '/', search: '', hash: '', }; @@ -38,16 +38,16 @@ export function parsePath(url: string): Partial { if (hashIdx > -1) { const hash = url.substring(hashIdx); parsedPath.hash = hash === '#' ? '' : hash; - url = url.substring(0, hashIdx); + pathname = pathname.substring(0, hashIdx); } const searchIdx = url.indexOf('?'); if (searchIdx > -1) { const search = url.substring(searchIdx); parsedPath.search = search === '?' ? '' : search; - url = url.substring(0, searchIdx); + pathname = pathname.substring(0, searchIdx); } - parsedPath.pathname = url; + parsedPath.pathname = pathname; return parsedPath; } @@ -116,12 +116,12 @@ export function stripBasename(path: string, prefix: string): string { export function createMemoryRecord(initVal: S, fn: (arg: S) => T) { let visitedRecord: T[] = [fn(initVal)]; - function getDelta(to: S, form: S): number { - let toIdx = visitedRecord.lastIndexOf(fn(to)); + function getDelta(toKey: S, formKey: S): number { + let toIdx = visitedRecord.lastIndexOf(fn(toKey)); if (toIdx === -1) { toIdx = 0; } - let fromIdx = visitedRecord.lastIndexOf(fn(form)); + let fromIdx = visitedRecord.lastIndexOf(fn(formKey)); if (fromIdx === -1) { fromIdx = 0; } diff --git a/packages/inula-router/src/router/NavLink.tsx b/packages/inula-router/src/router/NavLink.tsx index 37cbbeca..9a836e71 100644 --- a/packages/inula-router/src/router/NavLink.tsx +++ b/packages/inula-router/src/router/NavLink.tsx @@ -24,27 +24,42 @@ import { parsePath } from '../history/utils'; type NavLinkProps = { to: Partial | string | ((location: Location) => string | Partial); - isActive?: (match: Matched | null, location: Location) => boolean; + isActive?

(match: Matched

| null, location: Location): boolean; + exact?: boolean; + strict?: boolean; + sensitive?: boolean; + className?: string | ((isActive: boolean) => string); + activeClassName?: string; [key: string]: any; -} & LinkProps; +} & Omit; type Page = 'page'; function NavLink

(props: P) { - const { to, isActive, ...rest } = props; + const { to, isActive, exact, strict, sensitive, className, activeClassName, ...rest } = props; const context = useContext(Context); const toLocation = typeof to === 'function' ? to(context.location) : to; const { pathname } = typeof toLocation === 'string' ? parsePath(toLocation) : toLocation; - const match = pathname ? matchPath(context.location.pathname, pathname) : null; + const match = pathname ? matchPath(context.location.pathname, pathname, { + exact: exact, + strictMode: strict, + caseSensitive: sensitive, + }) : null; - const isLinkActive = match && isActive ? isActive(match, context.location) : false; + const isLinkActive = !!(isActive ? isActive(match, context.location) : match); + + let classNames = typeof className === 'function' ? className(isLinkActive) : className; + if (isLinkActive) { + classNames = [activeClassName, classNames].filter(Boolean).join(''); + } const page: Page = 'page'; const otherProps = { - 'aria-current': isLinkActive ? page : false, + className: classNames, + 'aria-current': isLinkActive ? page : undefined, ...rest, }; diff --git a/packages/inula-router/src/router/Route.tsx b/packages/inula-router/src/router/Route.tsx index fdf2db7f..ddd7c195 100644 --- a/packages/inula-router/src/router/Route.tsx +++ b/packages/inula-router/src/router/Route.tsx @@ -43,15 +43,19 @@ export type RouteProps

= {}, Path extends string = function Route = GetURLParams>(props: RouteProps) { const context = useContext(RouterContext); - const { computed, location, path } = props; - let { children, component, render } = props; + const { computed, location, path, component, render, strict, sensitive, exact } = props; + let { children } = props; let match: Matched

| null; const routeLocation = location || context.location; if (computed) { match = computed; } else if (path) { - match = matchPath

(routeLocation.pathname, path); + match = matchPath

(routeLocation.pathname, path, { + strictMode: strict, + caseSensitive: sensitive, + exact: exact, + }); } else { match = context.match; } diff --git a/packages/inula-router/src/router/Router.tsx b/packages/inula-router/src/router/Router.tsx index dd9791a1..1dcdf7e6 100644 --- a/packages/inula-router/src/router/Router.tsx +++ b/packages/inula-router/src/router/Router.tsx @@ -29,22 +29,27 @@ function Router

(props: P) { const { history, children = null } = props; const [location, setLocation] = useState(props.history.location); const pendingLocation = useRef(null); + const unListen = useRef void)>(null); + const isMount = useRef(false); // 在Router加载时就监听history地址变化,以保证在始渲染时重定向能正确触发 - const unListen = useRef void)>( - history.listen(arg => { + if (unListen.current === null) { + unListen.current = history.listen(arg => { pendingLocation.current = arg.location; - }), - ); + }); + } // 模拟componentDidMount和componentWillUnmount useLayoutEffect(() => { + isMount.current = true; if (unListen.current) { unListen.current(); } // 监听history中的位置变化 unListen.current = history.listen(arg => { - setLocation(arg.location); + if (isMount.current) { + setLocation(arg.location); + } }); if (pendingLocation.current) { @@ -53,6 +58,7 @@ function Router

(props: P) { return () => { if (unListen.current) { + isMount.current = false; unListen.current(); unListen.current = null; pendingLocation.current = null; diff --git a/packages/inula-router/src/router/matcher/parser.ts b/packages/inula-router/src/router/matcher/parser.ts index 0598066c..ea75e4e5 100644 --- a/packages/inula-router/src/router/matcher/parser.ts +++ b/packages/inula-router/src/router/matcher/parser.ts @@ -40,7 +40,7 @@ export type Matched

= { const defaultOption: Required = { // url匹配时是否大小写敏感 - caseSensitive: true, + caseSensitive: false, // 是否严格匹配url结尾的/ strictMode: false, // 是否完全精确匹配 diff --git a/packages/inula-router/src/router/withRouter.tsx b/packages/inula-router/src/router/withRouter.tsx index 3d7f58bd..04c588c8 100644 --- a/packages/inula-router/src/router/withRouter.tsx +++ b/packages/inula-router/src/router/withRouter.tsx @@ -20,10 +20,11 @@ import RouterContext from './context'; function withRouter(Component: C) { function ComponentWithRouterProp(props: any) { + const { wrappedComponentRef, ...rest } = props; const { history, location, match } = useContext(RouterContext); const routeProps = { history: history, location: location, match: match }; - return ; + return ; } return ComponentWithRouterProp; From b587af88b511f9c54e6ba2bb163bebfda42d8129 Mon Sep 17 00:00:00 2001 From: huangxuan Date: Fri, 1 Dec 2023 16:18:03 +0800 Subject: [PATCH 084/108] =?UTF-8?q?package.json=E5=85=BC=E5=AE=B9webpack5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/package.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/inula/package.json b/packages/inula/package.json index 2eb04d4b..54ae4908 100644 --- a/packages/inula/package.json +++ b/packages/inula/package.json @@ -24,5 +24,13 @@ "watch-test": "yarn test --watch --dev" }, "files": ["build/**/*", "README.md"], - "types": "./build/@types/index.d.ts" + "types": "./build/@types/index.d.ts", + "exports": { + ".": { + "default": "./index.js" + }, + "./package.json":"./package.json", + "./jsx-runtime": "./jsx-runtime.js", + "./jsx-dev-runtime": "./jsx-dev-runtime.js" + } } From ee39db737d932cffd096174b7105d4780de15abe Mon Sep 17 00:00:00 2001 From: HoikanChen Date: Sat, 2 Dec 2023 15:11:24 +0800 Subject: [PATCH 085/108] =?UTF-8?q?=E4=BF=AE=E6=94=B9lisence=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=A4=B9=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {Lisence => Licenses}/License | 0 .../Third Party Open Source Software Notice.docx | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {Lisence => Licenses}/License (100%) rename {Lisence => Licenses}/Third Party Open Source Software Notice.docx (100%) diff --git a/Lisence/License b/Licenses/License similarity index 100% rename from Lisence/License rename to Licenses/License diff --git a/Lisence/Third Party Open Source Software Notice.docx b/Licenses/Third Party Open Source Software Notice.docx similarity index 100% rename from Lisence/Third Party Open Source Software Notice.docx rename to Licenses/Third Party Open Source Software Notice.docx From 706c98c7a0a432b70a81dd2fcc149d3cec15660a Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 5 Dec 2023 19:38:17 +0800 Subject: [PATCH 086/108] =?UTF-8?q?[inula-request]=20=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E7=BB=84=E4=BB=B6=E7=B1=BB=E5=9E=8B=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-request/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/inula-request/index.ts b/packages/inula-request/index.ts index 85ca6ff4..dc2d0309 100644 --- a/packages/inula-request/index.ts +++ b/packages/inula-request/index.ts @@ -68,7 +68,7 @@ export { isAxiosError, }; -export type { IrRequestConfig, IrResponse, IrInstance, CancelTokenSource, IrProgressEvent } from './src/types/interfaces'; -export type { Method, ResponseType } from './src/types/types'; +export { IrRequestConfig, IrResponse, IrInstance, CancelTokenSource, IrProgressEvent } from './src/types/interfaces'; +export { Method, ResponseType } from './src/types/types'; export default inulaRequest; From 445e9e7757ef9e7f1fb3d325f2a3ba5c06481376 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 5 Dec 2023 19:48:12 +0800 Subject: [PATCH 087/108] =?UTF-8?q?[inula-request]=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=A7=A3=E6=9E=90=20params=20=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E4=B8=AD=20value=20=E4=B8=BA=E6=95=B0=E7=BB=84=E7=9A=84?= =?UTF-8?q?=E5=9C=BA=E6=99=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-request/src/utils/commonUtils/utils.ts | 13 ++++++++++++- .../utils/commonUtils/objectToQueryString.test.ts | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/inula-request/src/utils/commonUtils/utils.ts b/packages/inula-request/src/utils/commonUtils/utils.ts index 59ac6354..0096c66a 100644 --- a/packages/inula-request/src/utils/commonUtils/utils.ts +++ b/packages/inula-request/src/utils/commonUtils/utils.ts @@ -386,7 +386,18 @@ const convertToCamelCase = (str: string) => { function objectToQueryString(obj: Record) { return Object.keys(obj) - .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])) + .map(key => { + // params 中 value 为数组时需特殊处理,如:{ key: [1, 2, 3] } -> key[]=1&key[]=2&key[]=3 + if (Array.isArray(obj[key])) { + let urlPart = ''; + obj[key].forEach((value: string) => { + urlPart = `${urlPart}${key}[]=${value}&`; + return urlPart; + }); + return urlPart.slice(0, -1); + } + return encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]); + }) .join('&'); } diff --git a/packages/inula-request/tests/unitTest/utils/commonUtils/objectToQueryString.test.ts b/packages/inula-request/tests/unitTest/utils/commonUtils/objectToQueryString.test.ts index ea1c89ec..db98f676 100644 --- a/packages/inula-request/tests/unitTest/utils/commonUtils/objectToQueryString.test.ts +++ b/packages/inula-request/tests/unitTest/utils/commonUtils/objectToQueryString.test.ts @@ -53,7 +53,7 @@ describe('objectToQueryString function', () => { key5: { a: 'b' }, }; const expectedResult = - 'key1=string&key2=42&key3=true&key4=1%2C2%2C3&key5=%5Bobject%20Object%5D'; + 'key1=string&key2=42&key3=true&key4[]=1&key4[]=2&key4[]=3&key5=%5Bobject%20Object%5D'; const result = utils.objectToQueryString(input); expect(result).toBe(expectedResult); }); From cb7063ad82a3156633c79b6e2aa59cc55b71f70a Mon Sep 17 00:00:00 2001 From: huangxuan Date: Tue, 5 Dec 2023 19:53:56 +0800 Subject: [PATCH 088/108] revert merge request --- .../ComponentTest/FunctionComponent.test.js | 36 ------------------- packages/inula/src/renderer/hooks/HookMain.ts | 34 +----------------- .../src/renderer/hooks/UseReducerHook.ts | 1 - 3 files changed, 1 insertion(+), 70 deletions(-) diff --git a/packages/inula/scripts/__tests__/ComponentTest/FunctionComponent.test.js b/packages/inula/scripts/__tests__/ComponentTest/FunctionComponent.test.js index ef17a0aa..0b5d18a8 100644 --- a/packages/inula/scripts/__tests__/ComponentTest/FunctionComponent.test.js +++ b/packages/inula/scripts/__tests__/ComponentTest/FunctionComponent.test.js @@ -14,7 +14,6 @@ */ import * as Inula from '../../../src/index'; -const { useState } = Inula; describe('FunctionComponent Test', () => { it('渲染无状态组件', () => { const App = props => { @@ -90,39 +89,4 @@ describe('FunctionComponent Test', () => { Inula.render(, container); expect(container.querySelector('div').style['_values']['--max-segment-num']).toBe(10); }); - - it('函数组件渲染中重新发生setState不触发整个应用重新更新', () => { - function CountLabel({ count }) { - const [prevCount, setPrevCount] = useState(count); - const [trend, setTrend] = useState(null); - if (prevCount !== count) { - setPrevCount(count); - setTrend(count > prevCount ? 'increasing' : 'decreasing'); - } - return ( - <> -

{count}

- - {trend &&

The count is {trend}

} - - ); - } - - let count = 0; - function Child({ trend }) { - count++; - return
{trend}
; - } - - let update; - function App() { - const [count, setCount] = useState(0); - update = setCount; - return ; - } - - Inula.render(, container); - update(1); - expect(count).toBe(2); - }) }); diff --git a/packages/inula/src/renderer/hooks/HookMain.ts b/packages/inula/src/renderer/hooks/HookMain.ts index c2943f49..48f39f42 100644 --- a/packages/inula/src/renderer/hooks/HookMain.ts +++ b/packages/inula/src/renderer/hooks/HookMain.ts @@ -18,19 +18,12 @@ import type { VNode } from '../Types'; import { getLastTimeHook, setLastTimeHook, setCurrentHook, getNextHook } from './BaseHook'; import { HookStage, setHookStage } from './HookStage'; -const NESTED_UPDATE_LIMIT = 50; -// state updated in render phrase -let hasUpdatedInRender = false; function resetGlobalVariable() { setHookStage(null); setLastTimeHook(null); setCurrentHook(null); } -export function markUpdatedInRender() { - hasUpdatedInRender = true; -} - // hook对外入口 export function runFunctionWithHooks, Arg>( funcComp: (props: Props, arg: Arg) => any, @@ -52,14 +45,8 @@ export function runFunctionWithHooks, Arg>( setHookStage(HookStage.Update); } - let comp = funcComp(props, arg); + const comp = funcComp(props, arg); - if (hasUpdatedInRender) { - resetGlobalVariable(); - processing.oldHooks = processing.hooks; - setHookStage(HookStage.Update); - comp = runFunctionAgain(funcComp, props, arg); - } // 设置hook阶段为null,用于判断hook是否在函数组件中调用 setHookStage(null); @@ -76,22 +63,3 @@ export function runFunctionWithHooks, Arg>( return comp; } - -function runFunctionAgain, Arg>( - funcComp: (props: Props, arg: Arg) => any, - props: Props, - arg: Arg -) { - let reRenderTimes = 0; - let childElements; - while (hasUpdatedInRender) { - reRenderTimes++; - if (reRenderTimes > NESTED_UPDATE_LIMIT) { - throw new Error('Too many setState called in function component'); - } - hasUpdatedInRender = false; - childElements = funcComp(props, arg); - } - - return childElements; -} diff --git a/packages/inula/src/renderer/hooks/UseReducerHook.ts b/packages/inula/src/renderer/hooks/UseReducerHook.ts index e0ae2f3a..190d185f 100644 --- a/packages/inula/src/renderer/hooks/UseReducerHook.ts +++ b/packages/inula/src/renderer/hooks/UseReducerHook.ts @@ -21,7 +21,6 @@ import { setStateChange } from '../render/FunctionComponent'; import { getHookStage, HookStage } from './HookStage'; import type { VNode } from '../Types'; import { getProcessingVNode } from '../GlobalVar'; -import { markUpdatedInRender } from './HookMain'; // 构造新的Update数组 function insertUpdate(action: A, hook: Hook): Update { From 5b0ed0a5aa044b53a44d32440627795fd489be50 Mon Sep 17 00:00:00 2001 From: huangxuan Date: Tue, 5 Dec 2023 19:54:55 +0800 Subject: [PATCH 089/108] =?UTF-8?q?=E4=BF=AE=E5=A4=8DFragment=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E8=AE=BE=E7=BD=AEkey=E4=B8=8D=E7=94=9F=E6=95=88?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ComponentTest/FragmentComponent.test.js | 57 +++++++++++++++++++ .../src/renderer/diff/nodeDiffComparator.ts | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/packages/inula/scripts/__tests__/ComponentTest/FragmentComponent.test.js b/packages/inula/scripts/__tests__/ComponentTest/FragmentComponent.test.js index c91c5dac..6ac08b89 100644 --- a/packages/inula/scripts/__tests__/ComponentTest/FragmentComponent.test.js +++ b/packages/inula/scripts/__tests__/ComponentTest/FragmentComponent.test.js @@ -473,4 +473,61 @@ describe('Fragment', () => { expect(LogUtils.getNotClear()).toEqual([]); expect(container.textContent).toBe('1'); }); + + it('Fragment 设置相同的key不会重新渲染组件', () => { + const { useState, useRef, act, Fragment } = Inula; + let setFn; + const didMount = jest.fn(); + const didUpdate = jest.fn(); + + const App = () => { + const [list, setList] = useState([ + { text: 'Apple', id: 1 }, + { text: 'Banana', id: 2 }, + ]); + + setFn = setList; + + return ( + <> + {list.map(item => { + return ( + + + + ); + })} + + ); + }; + + const Child = ({ val }) => { + const mount = useRef(false); + useEffect(() => { + if (!mount.current) { + didMount(); + mount.current = true; + } else { + didUpdate(); + } + }); + + return
{val}
; + }; + + act(() => Inula.render(, container)); + + expect(didMount).toHaveBeenCalledTimes(2); + act(() => { + setFn([ + { text: 'Apple', id: 1 }, + { text: 'Banana', id: 2 }, + { text: 'Grape', id: 3 }, + ]); + }); + + // 数组前两项Key不变子组件更新,第三个子组件会挂载 + expect(didMount).toHaveBeenCalledTimes(3); + expect(didUpdate).toHaveBeenCalledTimes(2); + }); }); diff --git a/packages/inula/src/renderer/diff/nodeDiffComparator.ts b/packages/inula/src/renderer/diff/nodeDiffComparator.ts index 8de6f392..bb07e3ef 100644 --- a/packages/inula/src/renderer/diff/nodeDiffComparator.ts +++ b/packages/inula/src/renderer/diff/nodeDiffComparator.ts @@ -159,7 +159,7 @@ function getNewNode(parentNode: VNode, newChild: any, oldNode: VNode | null) { const key = oldNode !== null ? oldNode.key : newChild.key; resultNode = createFragmentVNode(key, newChild.props.children); } else { - resultNode = updateVNode(oldNode, newChild); + resultNode = updateVNode(oldNode, newChild.props.children); } break; } From 0022a6b55a3147d5b54f34a1ad8ed53e1089276c Mon Sep 17 00:00:00 2001 From: huangxuan Date: Tue, 5 Dec 2023 19:56:05 +0800 Subject: [PATCH 090/108] =?UTF-8?q?=E4=BF=AE=E5=A4=8DStrictMode=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E5=AF=BC=E8=87=B4=E5=AD=90=E7=BB=84=E4=BB=B6=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E4=B8=A2=E5=A4=B1=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ComponentTest/StrictMode.test.js | 58 +++++++++++++++++++ .../src/renderer/diff/nodeDiffComparator.ts | 10 ++-- 2 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 packages/inula/scripts/__tests__/ComponentTest/StrictMode.test.js diff --git a/packages/inula/scripts/__tests__/ComponentTest/StrictMode.test.js b/packages/inula/scripts/__tests__/ComponentTest/StrictMode.test.js new file mode 100644 index 00000000..a12fbed4 --- /dev/null +++ b/packages/inula/scripts/__tests__/ComponentTest/StrictMode.test.js @@ -0,0 +1,58 @@ +/* + * 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 Inula from '../../../src/index'; +import { getLogUtils } from '../jest/testUtils'; + +describe('StrictMode Component test', () => { + const LogUtils = getLogUtils(); + const { useState, useEffect, useRef, render, act } = Inula; + it('StrictMode is same to Fragment', () => { + const Parent = () => { + const [, setS] = useState('1'); + return ( + + + + + ); + }; + + const Child = () => { + const isMount = useRef(false); + + useEffect(() => { + if (!isMount.current) { + LogUtils.log('didMount'); + isMount.current = true; + } else { + LogUtils.log('didUpdate'); + } + }); + + return null; + }; + + act(() => render(, container)); + // 子组件初始化,会挂载一次 + expect(LogUtils.getAndClear()).toStrictEqual(['didMount']); + const button = container.querySelector('#btn'); + // 父组件State更新,子组件也会更新一次 + act(() => button.click()); + expect(LogUtils.getAndClear()).toStrictEqual(['didUpdate']); + }); +}); diff --git a/packages/inula/src/renderer/diff/nodeDiffComparator.ts b/packages/inula/src/renderer/diff/nodeDiffComparator.ts index bb07e3ef..e7161174 100644 --- a/packages/inula/src/renderer/diff/nodeDiffComparator.ts +++ b/packages/inula/src/renderer/diff/nodeDiffComparator.ts @@ -15,7 +15,7 @@ import type { VNode } from '../Types'; import { FlagUtils } from '../vnode/VNodeFlags'; -import { TYPE_COMMON_ELEMENT, TYPE_FRAGMENT, TYPE_PORTAL } from '../../external/JSXElementType'; +import { TYPE_COMMON_ELEMENT, TYPE_FRAGMENT, TYPE_PORTAL, TYPE_STRICT_MODE } from '../../external/JSXElementType'; import { DomText, DomPortal, Fragment, DomComponent } from '../vnode/VNodeTags'; import { updateVNode, @@ -35,9 +35,9 @@ enum DiffCategory { ARR_NODE = 'ARR_NODE', } -// 检查是不是被 FRAGMENT 包裹 -function isNoKeyFragment(child: any) { - return child != null && child.type === TYPE_FRAGMENT && child.key === null; +// 检查是不是被 FRAGMENT 或 StrictMode 包裹 +function isNoKeyFragmentOrStrictMode(child: any) { + return child != null && (child.type === TYPE_FRAGMENT || child.type === TYPE_STRICT_MODE) && child.key === null; } // 清除单个节点 @@ -631,7 +631,7 @@ export function createChildrenByDiff( newChild: any, isComparing: boolean ): VNode | null { - const isFragment = isNoKeyFragment(newChild); + const isFragment = isNoKeyFragmentOrStrictMode(newChild); newChild = isFragment ? newChild.props.children : newChild; // 1. 没有新节点,直接把vNode标记为删除 From 22036bc75d4f75a8ca7762cf011b922bed19c150 Mon Sep 17 00:00:00 2001 From: huangxuan Date: Tue, 5 Dec 2023 19:57:07 +0800 Subject: [PATCH 091/108] =?UTF-8?q?Dom=E4=B8=8A=E9=94=AE=E5=80=BC=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E9=9A=8F=E6=9C=BA=E5=AD=97=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/src/dom/DOMExternal.ts | 3 ++- packages/inula/src/dom/DOMInternalKeys.ts | 9 ++++++--- .../inula/src/dom/valueHandler/ValueChangeHandler.ts | 2 +- packages/inula/src/event/EventBinding.ts | 6 +++--- packages/inula/src/event/InulaEventMain.ts | 8 +++++++- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/inula/src/dom/DOMExternal.ts b/packages/inula/src/dom/DOMExternal.ts index 076709e9..12dea7b1 100644 --- a/packages/inula/src/dom/DOMExternal.ts +++ b/packages/inula/src/dom/DOMExternal.ts @@ -21,6 +21,7 @@ import { findDOMByClassInst } from '../renderer/vnode/VNodeUtils'; import { listenSimulatedDelegatedEvents } from '../event/EventBinding'; import { Callback } from '../renderer/Types'; import { InulaNode } from '../types'; +import { EVENT_KEY } from './DOMInternalKeys'; function createRoot(children: any, container: Container, callback?: Callback) { // 清空容器 @@ -89,7 +90,7 @@ function findDOMNode(domOrEle?: Element): null | Element | Text { // 情况根节点监听器 function removeRootEventLister(container: Container) { - const events = (container as any).$EV; + const events = (container as any)[EVENT_KEY]; if (events) { Object.keys(events).forEach(event => { const listener = events[event]; diff --git a/packages/inula/src/dom/DOMInternalKeys.ts b/packages/inula/src/dom/DOMInternalKeys.ts index a8727261..c4f1824f 100644 --- a/packages/inula/src/dom/DOMInternalKeys.ts +++ b/packages/inula/src/dom/DOMInternalKeys.ts @@ -22,9 +22,12 @@ import type { Container, Props } from './DOMOperator'; import { DomComponent, DomText, TreeRoot } from '../renderer/vnode/VNodeTags'; -const INTERNAL_VNODE = '_inula_VNode'; -const INTERNAL_PROPS = '_inula_Props'; -const INTERNAL_NONDELEGATEEVENTS = '_inula_NonDelegatedEvents'; +const randomKey = Math.random().toString(16).slice(2); +const INTERNAL_VNODE = `_inula_VNode_${randomKey}`; +const INTERNAL_PROPS = `_inula_Props_${randomKey}`; +const INTERNAL_NONDELEGATEEVENTS = `_inula_nonDelegatedEvents_${randomKey}`; +export const HANDLER_KEY = `_inula_valueChangeHandler_${randomKey}`; +export const EVENT_KEY = `_inula_ev_${randomKey}`; // 通过 VNode 实例获取 DOM 节点 export function getDom(vNode: VNode): Element | Text | null { diff --git a/packages/inula/src/dom/valueHandler/ValueChangeHandler.ts b/packages/inula/src/dom/valueHandler/ValueChangeHandler.ts index cd07c35c..262dc837 100644 --- a/packages/inula/src/dom/valueHandler/ValueChangeHandler.ts +++ b/packages/inula/src/dom/valueHandler/ValueChangeHandler.ts @@ -18,7 +18,7 @@ * 只有值发生变化时才会触发change事件。 */ -const HANDLER_KEY = '_valueChangeHandler'; +import { HANDLER_KEY } from '../DOMInternalKeys'; // 判断是否是 check 类型 function isCheckType(dom: HTMLInputElement): boolean { diff --git a/packages/inula/src/event/EventBinding.ts b/packages/inula/src/event/EventBinding.ts index f3e9f58e..52bfffcd 100644 --- a/packages/inula/src/event/EventBinding.ts +++ b/packages/inula/src/event/EventBinding.ts @@ -18,7 +18,7 @@ */ import { allDelegatedInulaEvents, portalDefaultDelegatedEvents, simulatedDelegatedEvents } from './EventHub'; import { isDocument } from '../dom/utils/Common'; -import { getNearestVNode, getNonDelegatedListenerMap } from '../dom/DOMInternalKeys'; +import { EVENT_KEY, getNearestVNode, getNonDelegatedListenerMap } from '../dom/DOMInternalKeys'; import { asyncUpdates, runDiscreteUpdates } from '../renderer/TreeBuilder'; import { handleEventMain } from './InulaEventMain'; import { decorateNativeEvent } from './EventWrapper'; @@ -73,8 +73,8 @@ export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) { const nativeFullName = isCapture ? nativeEvent + 'capture' : nativeEvent; // 事件存储在DOM节点属性,避免多个VNode(root和portal)对应同一个DOM, 造成事件重复监听 - currentRoot.realNode.$EV = currentRoot.realNode.$EV ?? {}; - const events = currentRoot.realNode.$EV; + currentRoot.realNode[EVENT_KEY] = currentRoot.realNode[EVENT_KEY] ?? {}; + const events = currentRoot.realNode[EVENT_KEY]; if (!events[nativeFullName]) { events[nativeFullName] = listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture); diff --git a/packages/inula/src/event/InulaEventMain.ts b/packages/inula/src/event/InulaEventMain.ts index cff523fc..817618f4 100644 --- a/packages/inula/src/event/InulaEventMain.ts +++ b/packages/inula/src/event/InulaEventMain.ts @@ -142,7 +142,13 @@ function triggerInulaEvents( const target = nativeEvent.target || nativeEvent.srcElement!; // 触发普通委托事件 - const listenerList: ListenerUnitList = getCommonListeners(nativeEvtName, vNode, nativeEvent, target, isCapture); + const listenerList: ListenerUnitList = getCommonListeners( + nativeEvtName, + vNode, + nativeEvent as MouseEvent, + target, + isCapture + ); let mouseEnterListeners: ListenerUnitList = []; if (inulaEventToNativeMap.get('onMouseEnter')!.includes(nativeEvtName)) { From bbabd5e8a5f2c21c0cd50eca8a11d518e57cb99a Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 5 Dec 2023 19:57:54 +0800 Subject: [PATCH 092/108] =?UTF-8?q?[inula-request]=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=8F=96=E6=B6=88=E8=AF=B7=E6=B1=82=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-request/src/request/fetchRequest.ts | 10 ++++++++-- .../src/request/processUploadProgress.ts | 18 +++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/inula-request/src/request/fetchRequest.ts b/packages/inula-request/src/request/fetchRequest.ts index 12160bdd..b2f598ee 100644 --- a/packages/inula-request/src/request/fetchRequest.ts +++ b/packages/inula-request/src/request/fetchRequest.ts @@ -168,8 +168,14 @@ export const fetchRequest = (config: IrRequestConfig): Promise => { } }) .catch((error: IrError) => { - const irError = new IrError(error.message, 'ERR_FETCH_FAILED', responseData.config, responseData.request, responseData); - reject(irError); + // fetch 在取消请求的极限场景会抛出 Failed to fetch 的 error,此时将其转为取消 error + if (signal?.aborted) { + const irError = new CancelError('request canceled', config); + reject(irError); + } else { + const irError = new IrError(error.message, 'ERR_FETCH_FAILED', responseData.config, responseData.request, responseData); + reject(irError); + } }); }) .catch((error: IrError) => { diff --git a/packages/inula-request/src/request/processUploadProgress.ts b/packages/inula-request/src/request/processUploadProgress.ts index 0ee4bea9..4e1586a4 100644 --- a/packages/inula-request/src/request/processUploadProgress.ts +++ b/packages/inula-request/src/request/processUploadProgress.ts @@ -15,6 +15,7 @@ import { IrProgressEvent, IrRequestConfig, IrResponse } from '../types/interfaces'; import IrError from '../core/IrError'; +import CancelError from '../cancel/CancelError'; function processUploadProgress( onUploadProgress: (progressEvent: IrProgressEvent) => void | null, @@ -95,7 +96,7 @@ function processUploadProgress( xhr.abort(); const errorMsg = config.timeoutErrorMessage ?? `timeout of ${config.timeout}ms exceeded`; throw new IrError(errorMsg, '', config, xhr, undefined); - } + }; } for (const header in config.headers) { @@ -106,6 +107,21 @@ function processUploadProgress( xhr.setRequestHeader(header, config.headers[header]); } } + + if (config.signal) { + const onCanceled = () => { + const irError = new CancelError('request canceled', config); + reject(irError); + xhr.abort(); + }; + + if (config.signal.aborted) { + onCanceled(); + } else { + config.signal.addEventListener('abort', onCanceled); + } + } + xhr.send(data); }; From e31cf7295d4661e6381a77b486577b9c36144a5b Mon Sep 17 00:00:00 2001 From: huangxuan Date: Tue, 5 Dec 2023 19:57:54 +0800 Subject: [PATCH 093/108] =?UTF-8?q?inulax=20createStore=EF=BC=8Cdispatch?= =?UTF-8?q?=20API=E5=85=BC=E5=AE=B9redux?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/src/inulax/adapters/redux.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/inula/src/inulax/adapters/redux.ts b/packages/inula/src/inulax/adapters/redux.ts index 97ee92f1..99e57748 100644 --- a/packages/inula/src/inulax/adapters/redux.ts +++ b/packages/inula/src/inulax/adapters/redux.ts @@ -91,6 +91,10 @@ function mergeData(state, data) { } export function createStore(reducer: Reducer, preloadedState?: any, enhancers?: StoreEnhancer): ReduxStoreHandler { + if (typeof preloadedState === 'function' && typeof enhancers === 'undefined') { + enhancers = preloadedState; + preloadedState = undefined; + } const store = createStoreX({ id: 'defaultStore', state: { stateWrapper: preloadedState }, @@ -107,6 +111,7 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?: return; } // NOTE: reducer should never return undefined, in this case, do not change state state.stateWrapper = result; + return action; }, }, options: { From e5598c440557a0d83cd83b3179f564d9f224f37d Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 5 Dec 2023 20:05:28 +0800 Subject: [PATCH 094/108] =?UTF-8?q?[inula]=20=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/scripts/rollup/rollup.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/inula/scripts/rollup/rollup.config.js b/packages/inula/scripts/rollup/rollup.config.js index 74f90045..45ee6f67 100644 --- a/packages/inula/scripts/rollup/rollup.config.js +++ b/packages/inula/scripts/rollup/rollup.config.js @@ -93,8 +93,8 @@ function genConfig(mode) { mode === 'production' && terser(), copy([ { - from: path.join(libDir, '/npm/index.jsx'), - to: path.join(outDir, 'index.jsx'), + from: path.join(libDir, '/npm/index.js'), + to: path.join(outDir, 'index.js'), } ]), ], From 706a9048562c8dd39b31917c67dcb4f7809f80db Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Wed, 6 Dec 2023 09:21:24 +0800 Subject: [PATCH 095/108] =?UTF-8?q?[inula-router]=20=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-router/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/inula-router/package.json b/packages/inula-router/package.json index 1c3356c6..91009d2d 100644 --- a/packages/inula-router/package.json +++ b/packages/inula-router/package.json @@ -1,6 +1,6 @@ { "name": "inula-router", - "version": "0.0.1", + "version": "0.0.3", "description": "router for inula framework, a part of inula-ecosystem", "main": "./router/cjs/router.js", "module": "./router/esm/router.js", @@ -67,7 +67,7 @@ "typescript": "4.9.3" }, "peerDependencies": { - "openinula": ">=0.0.10" + "openinula": ">=0.0.1" }, "browserslist": { "production": [ From 4785c3092c78cb962c2196638b636c7597534cae Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Wed, 6 Dec 2023 09:45:22 +0800 Subject: [PATCH 096/108] =?UTF-8?q?[inula-router]=20=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-router/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/inula-router/package.json b/packages/inula-router/package.json index 91009d2d..8bd78e8d 100644 --- a/packages/inula-router/package.json +++ b/packages/inula-router/package.json @@ -67,7 +67,7 @@ "typescript": "4.9.3" }, "peerDependencies": { - "openinula": ">=0.0.1" + "openinula": ">=0.1.1" }, "browserslist": { "production": [ From ee784d80cdfe6a7233473cfb801b914db9760fcb Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Wed, 6 Dec 2023 09:47:08 +0800 Subject: [PATCH 097/108] =?UTF-8?q?[all]=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/generators/Simple-app/templates/vite/package.json | 2 +- .../lib/generators/Simple-app/templates/webpack/package.json | 2 +- packages/inula-dev-tools/package.json | 2 +- packages/inula-intl/package.json | 2 +- packages/inula-request/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/create-inula/lib/generators/Simple-app/templates/vite/package.json b/packages/create-inula/lib/generators/Simple-app/templates/vite/package.json index e276cd82..33f0b06e 100644 --- a/packages/create-inula/lib/generators/Simple-app/templates/vite/package.json +++ b/packages/create-inula/lib/generators/Simple-app/templates/vite/package.json @@ -11,7 +11,7 @@ "author": "", "license": "ISC", "dependencies": { - "openinula": "^0.0.1" + "openinula": "^0.1.1" }, "devDependencies": { "@babel/core": "^7.21.4", diff --git a/packages/create-inula/lib/generators/Simple-app/templates/webpack/package.json b/packages/create-inula/lib/generators/Simple-app/templates/webpack/package.json index f2d2f0f4..047a0788 100644 --- a/packages/create-inula/lib/generators/Simple-app/templates/webpack/package.json +++ b/packages/create-inula/lib/generators/Simple-app/templates/webpack/package.json @@ -10,7 +10,7 @@ "author": "", "license": "ISC", "dependencies": { - "openinula": "^0.0.1" + "openinula": "^0.1.1" }, "devDependencies": { "@babel/core": "^7.21.4", diff --git a/packages/inula-dev-tools/package.json b/packages/inula-dev-tools/package.json index 3675f56d..46f655f8 100644 --- a/packages/inula-dev-tools/package.json +++ b/packages/inula-dev-tools/package.json @@ -45,7 +45,7 @@ "webpack-dev-server": "^4.7.4" }, "dependencies": { - "openinula": "^0.0.1", + "openinula": "^0.1.1", "flatted-object": "^0.1.2", "json-decycle": "^2.0.1", "lodash": "^4.17.21", diff --git a/packages/inula-intl/package.json b/packages/inula-intl/package.json index 910ca492..e370b1b0 100644 --- a/packages/inula-intl/package.json +++ b/packages/inula-intl/package.json @@ -23,7 +23,7 @@ "author": "", "license": "MulanPSL2", "peerDependencies": { - "openinula": "^0.0.1" + "openinula": "^0.1.1" }, "devDependencies": { "@babel/core": "7.21.3", diff --git a/packages/inula-request/package.json b/packages/inula-request/package.json index b2230d30..2964a78e 100644 --- a/packages/inula-request/package.json +++ b/packages/inula-request/package.json @@ -62,7 +62,7 @@ "webpack-dev-server": "^4.13.3" }, "peerDependencies": { - "openinula": "^0.0.1" + "openinula": "^0.1.1" }, "exclude": [ "node_modules" From 925a6de0e2de90fa186d45f7593fdd142b0d092a Mon Sep 17 00:00:00 2001 From: wangyu Date: Wed, 6 Dec 2023 15:30:02 +0800 Subject: [PATCH 098/108] add --- packages/inula-intl/build-type.js | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 packages/inula-intl/build-type.js diff --git a/packages/inula-intl/build-type.js b/packages/inula-intl/build-type.js new file mode 100644 index 00000000..0be14358 --- /dev/null +++ b/packages/inula-intl/build-type.js @@ -0,0 +1,50 @@ +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 nectFilePath = path.join(filePath, file); + const states = fs.lstatSync(nectFilePath); + if (states.isDirectory()) { + deleteFolder(nectFilePath); + } else { + fs.unlinkSync(nectFilePath); + } + }); + 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(folder => deleteFolder(folder)); + }, + }; +} + +function builderTypeConfig() { + return { + input: './build/@types/index.d.ts', + output: { + file: './build/@types/index.d.ts', + format: 'es', + }, + plugins: [dts(), cleanUp(['./build/@types/example', './build/@types/src'])], + }; +} + +export default [builderTypeConfig()]; From fb3180f2f2fbad54b4ab3a4262bda4939f7e8835 Mon Sep 17 00:00:00 2001 From: wangyu Date: Wed, 6 Dec 2023 15:37:37 +0800 Subject: [PATCH 099/108] modified: babel.config.js modified: example/App.tsx modified: example/index.tsx modified: index.ts modified: package.json modified: src/parser/Lexer.ts modified: src/parser/mappingRule.ts modified: src/parser/parseMappingRule.ts modified: src/parser/parser.ts modified: src/types/types.ts modified: tsconfig.json --- packages/inula-intl/babel.config.js | 18 ++--- packages/inula-intl/example/App.tsx | 2 +- packages/inula-intl/example/index.tsx | 4 +- packages/inula-intl/index.ts | 2 +- packages/inula-intl/package.json | 11 ++- packages/inula-intl/src/parser/Lexer.ts | 74 ++++++++++--------- packages/inula-intl/src/parser/mappingRule.ts | 2 +- .../inula-intl/src/parser/parseMappingRule.ts | 28 +++---- packages/inula-intl/src/parser/parser.ts | 17 ++--- packages/inula-intl/src/types/types.ts | 17 +++-- packages/inula-intl/tsconfig.json | 12 ++- 11 files changed, 97 insertions(+), 90 deletions(-) diff --git a/packages/inula-intl/babel.config.js b/packages/inula-intl/babel.config.js index ed7275da..df5b4f0b 100644 --- a/packages/inula-intl/babel.config.js +++ b/packages/inula-intl/babel.config.js @@ -13,7 +13,7 @@ * See the Mulan PSL v2 for more details. */ -const {preset} = require("./jest.config"); +const { preset } = require('./jest.config'); module.exports = { presets: [ [ @@ -23,19 +23,15 @@ module.exports = { browsers: ['> 1%', 'last 2 versions', 'not ie <= 8'], node: 'current', }, - useBuiltIns: 'usage', - corejs: 3, }, ], + ['@babel/preset-typescript'], [ - '@babel/preset-typescript', - ], - [ - "@babel/preset-react", + '@babel/preset-react', { - "runtime": "automatic", - "importSource": "openinula" - } - ] + runtime: 'automatic', + importSource: 'openinula', + }, + ], ], }; diff --git a/packages/inula-intl/example/App.tsx b/packages/inula-intl/example/App.tsx index 205a38f1..ba6166c7 100644 --- a/packages/inula-intl/example/App.tsx +++ b/packages/inula-intl/example/App.tsx @@ -39,7 +39,7 @@ const App = () => {
- +
diff --git a/packages/inula-intl/example/index.tsx b/packages/inula-intl/example/index.tsx index f4185e04..0678d203 100644 --- a/packages/inula-intl/example/index.tsx +++ b/packages/inula-intl/example/index.tsx @@ -17,9 +17,7 @@ import App from './App' function render() { Inula.render( - <> - - , + , document.querySelector('#root') as any ) } diff --git a/packages/inula-intl/index.ts b/packages/inula-intl/index.ts index da43b69b..776255b8 100644 --- a/packages/inula-intl/index.ts +++ b/packages/inula-intl/index.ts @@ -54,7 +54,7 @@ export default { IntlProvider: I18nProvider, injectIntl: injectIntl, RawIntlProvider: InjectProvider, -} +}; // 用于定义文本 export function defineMessages>(msgs: U): U { diff --git a/packages/inula-intl/package.json b/packages/inula-intl/package.json index e370b1b0..13d73b75 100644 --- a/packages/inula-intl/package.json +++ b/packages/inula-intl/package.json @@ -4,10 +4,11 @@ "description": "", "main": "build/intl.umd.js", "type": "commonjs", - "types": "build/index.d.ts", + "types": "build/@types/index.d.ts", "scripts": { "demo-serve": "webpack serve --mode=development", - "build": "rollup --config rollup.config.js", + "rollup-build": "rollup --config rollup.config.js && npm run build-types", + "build-types": "tsc -p tsconfig.json && rollup -c build-type.js", "test": "jest --config jest.config.js", "test-c": "jest --coverage" }, @@ -33,6 +34,7 @@ "@rollup/plugin-babel": "^6.0.3", "@rollup/plugin-node-resolve": "^7.1.3", "@rollup/plugin-typescript": "^11.0.0", + "rollup-plugin-dts": "^6.1.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@types/node": "^16.18.27", @@ -40,7 +42,6 @@ "babel": "^6.23.0", "babel-jest": "^29.5.0", "babel-loader": "^9.1.2", - "core-js": "3.31.0", "html-webpack-plugin": "^5.5.1", "jest": "29.3.1", "jest-environment-jsdom": "^29.5.0", @@ -56,8 +57,6 @@ "typescript": "4.9.3", "webpack": "^5.81.0", "webpack-cli": "^5.1.4", - "webpack-dev-server": "^4.13.3", - "react": "18.2.0", - "react-dom": "18.2.0" + "webpack-dev-server": "^4.13.3" } } diff --git a/packages/inula-intl/src/parser/Lexer.ts b/packages/inula-intl/src/parser/Lexer.ts index c233861e..f4a602b1 100644 --- a/packages/inula-intl/src/parser/Lexer.ts +++ b/packages/inula-intl/src/parser/Lexer.ts @@ -14,51 +14,45 @@ */ import ruleUtils from '../utils/parseRuleUtils'; -import { LexerInterface } from "../types/interfaces"; - -const getMatch = ruleUtils.checkSticky() - ? // 正则表达式具有 sticky 标志 - (regexp, buffer) => regexp.exec(buffer) - : // 正则表达式具有 global 标志,匹配的字符串长度为 0,则表示匹配失败 - (regexp, buffer) => (regexp.exec(buffer)[0].length === 0 ? null : regexp.exec(buffer)); +import { LexerInterface } from '../types/interfaces'; class Lexer implements LexerInterface { readonly startState: string; readonly states: Record; private buffer: string = ''; private stack: string[] = []; - private index; - private line; - private col; - private queuedText; - private state; - private groups; - private error; + private index: number = 0; + private line: number = 1; + private col: number = 1; + private queuedText: string = ''; + private state: string = ''; + private groups: string[] = []; + private error: Record | undefined; private regexp; - private fast; - private queuedGroup; - private value; + private fast: object = {}; + private queuedGroup: string | null = ''; + private value: string = ''; - constructor(states, state) { - this.startState = state; - this.states = states; + constructor(unionReg: Record, startState: string) { + this.startState = startState; + this.states = unionReg; this.buffer = ''; this.stack = []; this.reset(); } - public reset(data?, info?) { + public reset(data?: string) { this.buffer = data || ''; this.index = 0; - this.line = info ? info.line : 1; - this.col = info ? info.col : 1; - this.queuedText = info ? info.queuedText : ''; - this.setState(info ? info.state : this.startState); - this.stack = info && info.stack ? info.stack.slice() : []; + this.line = 1; + this.col = 1; + this.queuedText = ''; + this.setState(this.startState); + this.stack = []; return this; } - private setState(state) { + private setState(state: string) { if (!state || this.state === state) { return; } @@ -71,15 +65,15 @@ class Lexer implements LexerInterface { } private popState() { - this.setState(this.stack.pop()); + this.setState(this.stack.pop()); } - private pushState(state) { + private pushState(state: string) { this.stack.push(this.state); this.setState(state); } - private getGroup(match) { + private getGroup(match: Record) { const groupCount = this.groups.length; for (let i = 0; i < groupCount; i++) { if (match[i + 1] !== undefined) { @@ -127,7 +121,7 @@ class Lexer implements LexerInterface { const group = this.getGroup(match); const text = match[0]; - if (error.fallback && match.index !== index) { + if (error?.fallback && match.index !== index) { this.queuedGroup = group; this.queuedText = text; return this.getToken(error, buffer.slice(index, match.index), index); @@ -136,7 +130,14 @@ class Lexer implements LexerInterface { return this.getToken(group, text, index); } - private getToken(group, text, offset) { + /** + * 獲取Token + * @param group 解析模板后獲得的屬性值 + * @param text 文本屬性的信息 + * @param offset 偏移量 + * @private + */ + private getToken(group: any, text: string, offset: number) { let lineNum = 0; let last = 1; // 最后一个换行符的索引位置 if (group.lineBreaks) { @@ -192,9 +193,14 @@ class Lexer implements LexerInterface { next: (): IteratorResult => { const token = this.next(); return { value: token, done: !token } as IteratorResult; - } - } + }, + }; } } +const getMatch = ruleUtils.checkSticky() + ? // 正则表达式具有 sticky 标志 + (regexp, buffer) => regexp.exec(buffer) + : // 正则表达式具有 global 标志,匹配的字符串长度为 0,则表示匹配失败 + (regexp, buffer) => (regexp.exec(buffer)[0].length === 0 ? null : regexp.exec(buffer)); export default Lexer; diff --git a/packages/inula-intl/src/parser/mappingRule.ts b/packages/inula-intl/src/parser/mappingRule.ts index 962816ca..54bee482 100644 --- a/packages/inula-intl/src/parser/mappingRule.ts +++ b/packages/inula-intl/src/parser/mappingRule.ts @@ -70,5 +70,5 @@ const select: Record = { export const mappingRule: Record = { body, arg, - select + select, }; diff --git a/packages/inula-intl/src/parser/parseMappingRule.ts b/packages/inula-intl/src/parser/parseMappingRule.ts index 55e9af13..9323378c 100644 --- a/packages/inula-intl/src/parser/parseMappingRule.ts +++ b/packages/inula-intl/src/parser/parseMappingRule.ts @@ -16,16 +16,16 @@ import Lexer from './Lexer'; import { mappingRule } from './mappingRule'; import ruleUtils from '../utils/parseRuleUtils'; -import { RawToken } from "../types/types"; +import { RawToken } from '../types/types'; const defaultErrorRule = ruleUtils.getRuleOptions('error', { lineBreaks: true, shouldThrow: true }); // 解析规则并生成词法分析器所需的数据结构,以便进行词法分析操作 function parseRules(rules: Record, hasStates: boolean): Record { let errorRule: Record | null = null; - const fast = {}; - let enableFast = true; - let unicodeFlag = null; + const fast: object = {}; + let enableFast: boolean = true; + let unicodeFlag: boolean | null = null; const groups: Record[] = []; const parts: string[] = []; @@ -108,11 +108,11 @@ export function checkStateGroup(group: Record, name: string, map: R } // 将国际化解析规则注入分词器中 -function parseMappingRule(mappingRule: Record, start?: string): Lexer { +function parseMappingRule(mappingRule: Record, startState?: string): Lexer { const keys = Object.getOwnPropertyNames(mappingRule); - if (!start) { - start = keys[0]; + if (!startState) { + startState = keys[0]; } // 将每个状态的规则解析为规则数组,并存储在 ruleMap 对象中 @@ -153,27 +153,27 @@ function parseMappingRule(mappingRule: Record, start?: string): Lex } } - const map = {}; + const mappingAllRules = {}; - // 将规则映射为词法分析器数据结构,并存储在 map 对象中 + // 将规则映射为词法分析器数据结构,并存储在 mappingAllRules 对象中 keys.forEach(key => { - map[key] = parseRules(ruleMap[key], true); + mappingAllRules[key] = parseRules(ruleMap[key], true); }); // 检查状态组中的规则是否正确引用了其他状态 keys.forEach(name => { - const state = map[name]; + const state = mappingAllRules[name]; const groups = state.groups; groups.forEach(group => { - checkStateGroup(group, name, map); + checkStateGroup(group, name, mappingAllRules); }); const fastKeys = Object.getOwnPropertyNames(state.fast); fastKeys.forEach(fastKey => { - checkStateGroup(state.fast[fastKey], name, map); + checkStateGroup(state.fast[fastKey], name, mappingAllRules); }); }); - return new Lexer(map, start); + return new Lexer(mappingAllRules, startState); } function processFast(match, fast: {}, options) { diff --git a/packages/inula-intl/src/parser/parser.ts b/packages/inula-intl/src/parser/parser.ts index 76a6f9fd..cf885f67 100644 --- a/packages/inula-intl/src/parser/parser.ts +++ b/packages/inula-intl/src/parser/parser.ts @@ -17,7 +17,7 @@ import { lexer } from './parseMappingRule'; import { RawToken, Token } from '../types/types'; import { DEFAULT_PLURAL_KEYS } from '../constants'; import { Content, FunctionArg, PlainArg, Select, TokenContext } from '../types/interfaces'; -import Lexer from "./Lexer"; +import Lexer from './Lexer'; const getContext = (lt: Record): TokenContext => ({ offset: lt.offset, @@ -29,15 +29,14 @@ const getContext = (lt: Record): TokenContext => ({ export const checkSelectType = (value: string): boolean => { return value === 'plural' || value === 'select' || value === 'selectordinal'; -} +}; class Parser { - lexer: Lexer; cardinalKeys: string[] = DEFAULT_PLURAL_KEYS; ordinalKeys: string[] = DEFAULT_PLURAL_KEYS; constructor(message: string) { - this.lexer = lexer.reset(message); + lexer.reset(message); } isSelectKeyValid(token: RawToken, type: Select['type'], value: string) { @@ -60,7 +59,7 @@ class Parser { isPlural = true; } - for (const token of this.lexer) { + for (const token of lexer) { switch (token.type) { case 'offset': { if (type === 'select') { @@ -97,7 +96,7 @@ class Parser { parseToken(token: RawToken, isPlural: boolean): PlainArg | FunctionArg | Select { const context = getContext(token); - const nextToken = this.lexer.next(); + const nextToken = lexer.next(); if (!nextToken) { throw new Error('The message end position is invalid.'); @@ -111,7 +110,7 @@ class Parser { return { type: 'argument', arg: token.value, ctx: context }; } case 'func-simple': { - const end = this.lexer.next(); + const end = lexer.next(); if (!end) { throw new Error('The message end position is invalid.'); } @@ -159,7 +158,7 @@ class Parser { const tokens: any[] = []; let content: string | Content | null = null; - for (const token of this.lexer) { + for (const token of lexer) { if (token.type === 'argument') { if (content) { content = null; @@ -175,7 +174,7 @@ class Parser { } else if (token.type === 'doubleapos') { tokens.push(token.value); } else if (token.type === 'quoted') { - tokens.push(token.value) + tokens.push(token.value); } else if (token.type === 'content') { tokens.push(token.value); } else { diff --git a/packages/inula-intl/src/types/types.ts b/packages/inula-intl/src/types/types.ts index 2a7e463b..d5607ab6 100644 --- a/packages/inula-intl/src/types/types.ts +++ b/packages/inula-intl/src/types/types.ts @@ -21,9 +21,10 @@ import { Select, FunctionArg, I18nContextProps, - configProps + configProps, + InjectedIntl, } from './interfaces'; -import I18n from "../core/I18n"; +import I18n from '../core/I18n'; export type Error = string | ((message, id, context) => string); @@ -71,11 +72,15 @@ export type RawToken = { col: number; }; -export type I18nProviderProps = I18nContextProps & configProps +export type I18nProviderProps = I18nContextProps & configProps; export type IntlType = { i18n: I18n; - formatMessage: Function, - formatNumber: Function, - formatDate: Function, + formatMessage: Function; + formatNumber: Function; + formatDate: Function; +}; + +export interface InjectedIntlProps { + intl: InjectedIntl; } diff --git a/packages/inula-intl/tsconfig.json b/packages/inula-intl/tsconfig.json index 2ea73c31..f3ff266e 100644 --- a/packages/inula-intl/tsconfig.json +++ b/packages/inula-intl/tsconfig.json @@ -31,6 +31,7 @@ "declaration": true, "experimentalDecorators": true, "downlevelIteration": true, + "declarationDir": "./build/@types", // 赋值为空数组使@types/node不会起作用 "lib": [ "dom", @@ -53,19 +54,22 @@ } }, "include": [ - "./src/**/*", - "./src/format/**/*.ts", - "./example/**/*" + "./index.ts" ], "exclude": [ "node_modules", "lib", "**/*.spec.ts", - "dev" + "dev", + "./example/**/*", + "./tsconfig.json" ], "types": [ "node", "jest", "@testing-library/jest-dom" + ], + "files": [ + "./index.ts" ] } From 955b2cb5740fa4431081c64897e5a54e83f53366 Mon Sep 17 00:00:00 2001 From: huangxuan Date: Wed, 6 Dec 2023 16:01:07 +0800 Subject: [PATCH 100/108] =?UTF-8?q?[inulax]=20isSame=E6=AF=94=E8=BE=83?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula/src/inulax/CommonUtils.ts | 5 +++-- packages/inula/src/inulax/adapters/reduxReact.ts | 12 ++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/inula/src/inulax/CommonUtils.ts b/packages/inula/src/inulax/CommonUtils.ts index ccbb182f..5e309ca4 100644 --- a/packages/inula/src/inulax/CommonUtils.ts +++ b/packages/inula/src/inulax/CommonUtils.ts @@ -76,9 +76,10 @@ export function isSame(x: unknown, y: unknown): boolean { if (typeof x !== typeof y) { return false; } - // 如果两个对象都是null或undefined,直接返回true + // 此时x和y类型一致,若都为undefined或null返回true,但也有可能此时为一个对象和null,这种情况直接比较 + // typeof null === 'object' if (x == null || y == null) { - return true; + return x === y; } // 如果两个对象都是基本类型,比较他们的值是否相等 if (typeof x !== 'object') { diff --git a/packages/inula/src/inulax/adapters/reduxReact.ts b/packages/inula/src/inulax/adapters/reduxReact.ts index 32eff8d0..2b5a9dae 100644 --- a/packages/inula/src/inulax/adapters/reduxReact.ts +++ b/packages/inula/src/inulax/adapters/reduxReact.ts @@ -103,15 +103,11 @@ export function connect( dispatchProps, ownProps ): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps } as unknown as MergedProps), - options?: ConnectOption + options: ConnectOption = {}, ): Connector { - if (!options) { - options = {}; - } - //this component should bear the type returned from mapping functions return (Component: OriginalComponent): WrappedComponent => { - const useStore = createStoreHook(options?.context || DefaultContext); + const useStore = createStoreHook(options.context || DefaultContext); //this component should mimic original type of component used const Wrapper: WrappedComponent = (props: OwnProps) => { @@ -131,7 +127,7 @@ export function connect( }); let mappedState: StateProps; - if (options?.areStatesEqual) { + if (options.areStatesEqual) { if (options.areStatesEqual(previous.current.state, state)) { mappedState = previous.current.mappedState as StateProps; } else { @@ -168,7 +164,7 @@ export function connect( return createElement(Component, mergedProps); }; - if (options?.forwardRef) { + if (options.forwardRef) { const forwarded = forwardRef((props, ref) => { return Wrapper({ ...props, ref: ref }); }); From 6595eb7044a4aa4d211d9dfe51e6aabc401485cf Mon Sep 17 00:00:00 2001 From: yangxuting Date: Thu, 7 Dec 2023 10:33:34 +0800 Subject: [PATCH 101/108] =?UTF-8?q?=E4=BF=AE=E5=A4=8DReadme=E5=9B=BE?= =?UTF-8?q?=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- structure.png | Bin 0 -> 143116 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 structure.png diff --git a/README.md b/README.md index 7b99c861..284e9058 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## 技术架构 -![输入图片说明](https://gitee.com/openInula/inula-docs/raw/master/static/img/structure.png) +![](./structure.png) ### 核心能力 diff --git a/structure.png b/structure.png new file mode 100644 index 0000000000000000000000000000000000000000..66417d54566d84ce619428c4febdcd3e930263c4 GIT binary patch literal 143116 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

waSqWK%Wl z`*eEQe3tb@zC=eV;$oFeVhbJ}5%<@T9Ize`TXYsqr@zd`X=X)L4Q?(?hfrui&N4q| zso{?x=$ni03Byl1jS3bSaF-tmI9-@S!(`K=sO}3Ut&PruTLKDl9tW0M?BNU!F-MoX zfU=y$0yUB8-%ophQ4T18)3(*nN_+@-UELUU9 zzQh0Fw}$xRy<#de5lropfhtjCetetR?<~F7TNcf$tZ^4P1%mAade7aah0feIjb~b3 z12`tuE4~zZR#pVh5IQBqlkS)kbt8O`7!M|pNK^NYUNk-g2yRX%#gJ+`HMh z6>fCwW(lprzmI+0oZKPjxIyD2r&|9q4$|&BZ=N~_t&e~WeJ|)+sPs&k*Z;7n)H5RL zwlUXTX1_h5w>=*z{3=`akw~+jCFR27?~&7vRJq=2frA-fD>OzjBf5@Eu1r&-EvIP^K+^bemMJ}cFXBHOGhoP<%#=F^Sf>v?!7=EjRFp3(|R`8 z%EWEWdnG7DcQfPfv*grn4}#;_<)Ci{v7WDbH~nFfZ$!VI)GfD}$qGQ7@^)JvxVQbL zp}+Aian;Qi0Vn?Y?U6e0G*@;8cKb{0isQ^mfOvrCs?OtU`=6NVDzsg)5-GcB?!Wg{ z_O_LojG^~R5JnF8$W5@rR1`+Eb%)R-TYaF-T)Aq4uot=3=1`_o(?457)7wBftH_1)LsLITz zBoG46R-H3vKNdB}U?avAPcLlg+ImEnVX^uw$+BY0<@X z^%<&;GnTCxM=J7icR0Oep_y)srziJ|)~#T>m|E>deh|$Rfa7>Zf@e9ti-XXfY~Hc@{8rb}*Ta%Jm9b~v0L5hQ zGcdvhLZyhE3I?)rpGZLZlls^EE>I2QIU;+-QTG{i+_CEevgRHmvT=-^=+nIy-Yri4 z`1z@T5Oe+u%+sqn;$^x>vG&TIpcY7w1ApCO%f)fOz$5tm@Jo~x{DG-!xBOZYMhs|+ z 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%0D`P!bg?s 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 Date: Mon, 11 Dec 2023 09:37:20 +0800 Subject: [PATCH 102/108] =?UTF-8?q?=E6=9B=B4=E6=96=B0readme=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- structure.png | Bin 143116 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 structure.png diff --git a/README.md b/README.md index 284e9058..48b73535 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## 技术架构 -![](./structure.png) +![](https://openinula-website.obs.ap-southeast-1.myhuaweicloud.com/misc/structure.png) ### 核心能力 diff --git a/structure.png b/structure.png deleted file mode 100644 index 66417d54566d84ce619428c4febdcd3e930263c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 143116 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

waSqWK%Wl z`*eEQe3tb@zC=eV;$oFeVhbJ}5%<@T9Ize`TXYsqr@zd`X=X)L4Q?(?hfrui&N4q| zso{?x=$ni03Byl1jS3bSaF-tmI9-@S!(`K=sO}3Ut&PruTLKDl9tW0M?BNU!F-MoX zfU=y$0yUB8-%ophQ4T18)3(*nN_+@-UELUU9 zzQh0Fw}$xRy<#de5lropfhtjCetetR?<~F7TNcf$tZ^4P1%mAade7aah0feIjb~b3 z12`tuE4~zZR#pVh5IQBqlkS)kbt8O`7!M|pNK^NYUNk-g2yRX%#gJ+`HMh z6>fCwW(lprzmI+0oZKPjxIyD2r&|9q4$|&BZ=N~_t&e~WeJ|)+sPs&k*Z;7n)H5RL zwlUXTX1_h5w>=*z{3=`akw~+jCFR27?~&7vRJq=2frA-fD>OzjBf5@Eu1r&-EvIP^K+^bemMJ}cFXBHOGhoP<%#=F^Sf>v?!7=EjRFp3(|R`8 z%EWEWdnG7DcQfPfv*grn4}#;_<)Ci{v7WDbH~nFfZ$!VI)GfD}$qGQ7@^)JvxVQbL zp}+Aian;Qi0Vn?Y?U6e0G*@;8cKb{0isQ^mfOvrCs?OtU`=6NVDzsg)5-GcB?!Wg{ z_O_LojG^~R5JnF8$W5@rR1`+Eb%)R-TYaF-T)Aq4uot=3=1`_o(?457)7wBftH_1)LsLITz zBoG46R-H3vKNdB}U?avAPcLlg+ImEnVX^uw$+BY0<@X z^%<&;GnTCxM=J7icR0Oep_y)srziJ|)~#T>m|E>deh|$Rfa7>Zf@e9ti-XXfY~Hc@{8rb}*Ta%Jm9b~v0L5hQ zGcdvhLZyhE3I?)rpGZLZlls^EE>I2QIU;+-QTG{i+_CEevgRHmvT=-^=+nIy-Yri4 z`1z@T5Oe+u%+sqn;$^x>vG&TIpcY7w1ApCO%f)fOz$5tm@Jo~x{DG-!xBOZYMhs|+ z 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%0D`P!bg?s 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 Date: Tue, 19 Dec 2023 11:23:44 +0800 Subject: [PATCH 103/108] =?UTF-8?q?[intl][create]=20=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/create-inula/package.json | 2 +- packages/inula-intl/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/create-inula/package.json b/packages/create-inula/package.json index 88eac59c..251ecd60 100644 --- a/packages/create-inula/package.json +++ b/packages/create-inula/package.json @@ -1,6 +1,6 @@ { "name": "create-inula", - "version": "0.0.6", + "version": "0.0.8", "description": "", "main": "index.js", "bin": { diff --git a/packages/inula-intl/package.json b/packages/inula-intl/package.json index 13d73b75..a55c28d1 100644 --- a/packages/inula-intl/package.json +++ b/packages/inula-intl/package.json @@ -1,6 +1,6 @@ { "name": "inula-intl", - "version": "0.0.3", + "version": "0.0.4", "description": "", "main": "build/intl.umd.js", "type": "commonjs", From d5780d248d2e8f1117381f321d4947006d55fe58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E7=85=A6=E5=BA=AD?= <990815_yxt@sina.com> Date: Thu, 21 Dec 2023 07:52:18 +0000 Subject: [PATCH 104/108] =?UTF-8?q?=E6=9B=B4=E6=96=B0Readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 71 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 48b73535..d3243555 100644 --- a/README.md +++ b/README.md @@ -8,66 +8,71 @@ ![](https://openinula-website.obs.ap-southeast-1.myhuaweicloud.com/misc/structure.png) -### 核心能力 +## 核心能力 -#### 响应式API(实验性功能,可在reactive分支查看代码或使用npm仓中experiment版本体验) +### 响应式API -* openInula 通过最小化重新渲染的范围,从而进行高效的UI渲染。这种方式避免了虚拟 DOM 的开销,使得 openInula 在性能方面表现出色。 -* openInula 通过比较变化前后的 JavaScript 对象以细粒度的依赖追踪机制来实现响应式更新,无需用户过度关注性能优化。 -* 简洁API: - 1. openInula 提供了两组简洁直观的API--响应式 API 和与 React 一致的传统 API,使得开发者可以轻松地构建复杂的交互式界面。 - 2. openInula 简洁的 API 极大降低了开发者的学习成本,开发者使用响应式API可以快速构建高效的前端界面。 +openInula 通过监听状态变量的变化,以细粒度的依赖追踪机制来实现响应式更新,避免了虚拟 DOM 的开销。通过最小化重新渲染的范围,从而进行高效的UI渲染。无需用户过度关注性能优化。 -#### 兼容 ReactAPI +>(实验性功能,可在 `reactive` 分支查看代码或使用 npm 仓中 experimental 版本体验) -* 与React保持一致、可以无缝支持 React 生态。 -* 使用传统 API 可以无缝将 React 项目切换至 openInula,React 应用可零修改切换至 openInula。 +### 兼容 React API + +提供与 React 一致的 API,完全支持 React 生态,可将 React 应用可零修改切换至 openInula。 ### openInula 配套组件 -#### 状态管理器 → inula-X +#### 状态管理器 inula-X + +inula-X 是 openInula 默认提供的状态管理器。无需额外引入三方库,就可以简单实现跨组件/页面共享状态。 -inula-X 是 openInula 默认提供的状态管理器,无需额外引入三方库,就可以简单实现跨组件/页面共享状态。 inula-X 与 Redux 相比,可创建多个 Store,不需要在 Reducer 中返回 state 并且简化了 Action 和 Reducer 的创建步骤,原生支持异步能力,组件能做到精准重渲染。inula-X 均可使用函数组件、class 组件,能提供 redux 的适配接口及支持响应式的特点。 -#### 路由 → inula-router +#### 路由 inula-router -inula-router 是 openInula 生态组建的一部分,为 openInula 提供前端路由的能力,是构建大型应用必要组件。 -inula-router 涵盖 react-router、history、connect-react-router 的功能。 +inula-router 为 openInula 提供前端路由的能力,是构建大型应用必要组件,涵盖 react-router、history、connect-react-router 的功能。 -#### 请求 → inula-request +#### 请求 inula-request -inula-request 是 openInula 生态组件,涵盖常见的网络请求方式,并提供动态轮询钩子函数给用户更便捷的定制化请求体验。 +inula-request 是 openInula 的网络请求组件,不仅涵盖常见的网络请求方式,还提供动态轮询钩子函数给用户更便捷的定制化请求体验。 -#### 国际化 → inula-intl +#### 国际化 inula-intl -inula-intl 是基于 openInula 生态组件,其主要提供了国际化功能,涵盖了基本的国际化组件和钩子函数,便于用户在构建国际化能力时方便操作。 +inula-intl 是基于 openInula 的国际化组件,涵盖了基本的国际化组件和钩子函数,允许用户更方便地构建国际化能力。 -#### 调试工具 → inula-dev-tools +#### 调试工具 inula-dev-tools inula-dev-tools 是一个为 openInula 开发者提供的强大工具集,能够方便地查看和编辑组件树、管理应用状态以及进行性能分析,极大提高了开发效率和诊断问题的便捷性。 -#### 脚手架 → inula-cli +#### 脚手架 create-inula -inula-cli 是一套针对 openInula 的编译期插件,它支持代码优化、JSX 语法转换以及代码分割,有助于提高应用的性能、可读性和可维护性。 +create-inula 是一套用于创建 openInula 项目的脚手架工具。它预置了一系列项目模板,允许开发者通过命令行按需快速生成可运行的项目代码。 -## openInula 文档 +## 参与贡献 + +我们鼓励开发者以各种方式参与代码贡献、生态拓展或文档反馈,献您的原创内容,详细请参考[贡献指南](https://docs.openinula.net/docs/%E8%B4%A1%E7%8C%AE%E6%8C%87%E5%8D%97)。 + +### 官方链接 欢迎访问 openInula 官网与文档仓库,参与 openInula 开发者文档开源项目,与我们一起完善开发者文档。 -+ openInula 官网地址:[https://www.openinula.net/](https://www.openinula.net/) -+ openInula 文档站地址:[https://docs.openinula.net/](https://docs.openinula.net/) +* openInula 官网:[https://www.openinula.net/](https://www.openinula.net/) +* openInula 文档:[https://docs.openinula.net/](https://docs.openinula.net/) +* openInula 仓库地址:[https://gitee.com/openinula/inula](https://gitee.com/openinula/inula) +* openInula 社提案备忘录(RFC):[https://gitee.com/openInula/rfcs](https://gitee.com/openInula/rfcs) -## 代码仓地址 +### 社区贡献者案例 -openInula 仓库地址:[https://gitee.com/openinula](https://gitee.com/openinula) +**[`umi-inula`](https://gitee.com/congxiaochen/inula)** -## 如何参与 +基于 umijs 与 openInula 的开发框架,集成官方组件与UI、AIGC等功能,开箱即用。 -**参与贡献** -欢迎您参与贡献,我们鼓励开发者以各种方式参与文档反馈和贡献。 +**[`voerkaiI18l`](https://github.com/zhangfisher/voerka-i18n/)** -您可以对现有文档进行评价、简单更改、反馈文档质量问题、贡献您的原创内容,详细请参考[贡献指南](https://docs.openinula.net/docs/%E8%B4%A1%E7%8C%AE%E6%8C%87%E5%8D%97)。 +适用于多框架的 JavaScript 国际化解决方案,提供对 openInula 的适配。 + +- [适配示例](https://gitee.com/link?target=https%3A%2F%2Fgithub.com%2Fzhangfisher%2Fvoerka-i18n%2Ftree%2Fmaster%2Fexamples%2Fopeninula) +- [适配文档](https://gitee.com/link?target=https%3A%2F%2Fzhangfisher.github.io%2Fvoerka-i18n%2F%23%2Fzh%2Fguide%2Fintegration%2Fopeninula) ## 许可协议 @@ -75,6 +80,8 @@ openInula 主要遵循 [Mulan Permissive Software License v2](http://license.cos ## 联系方式 -team@inulajs.org +[官方邮箱](team@inulajs.org) +微信公众号 +![](https://www.openinula.net/assets/qrcode.inula-02f99d58.jpg) From b2a08e8c6c59d616adcdbdef4881a29d9c886dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E7=85=A6=E5=BA=AD?= <990815_yxt@sina.com> Date: Thu, 21 Dec 2023 07:56:37 +0000 Subject: [PATCH 105/108] =?UTF-8?q?=E6=9B=B4=E6=96=B0Readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d3243555..914f403b 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,8 @@ openInula 主要遵循 [Mulan Permissive Software License v2](http://license.cos ## 联系方式 -[官方邮箱](team@inulajs.org) +* 官方邮箱: [team@inulajs.org](mailto:team@inulajs.org) -微信公众号 +* 微信公众号: ![](https://www.openinula.net/assets/qrcode.inula-02f99d58.jpg) From 32e509f9deced88c9b65cf2657f337c9d755708f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E7=85=A6=E5=BA=AD?= <990815_yxt@sina.com> Date: Thu, 21 Dec 2023 08:51:10 +0000 Subject: [PATCH 106/108] update README.md. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 杨煦庭 <990815_yxt@sina.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 914f403b..38051ca8 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ create-inula 是一套用于创建 openInula 项目的脚手架工具。它预 基于 umijs 与 openInula 的开发框架,集成官方组件与UI、AIGC等功能,开箱即用。 -**[`voerkaiI18l`](https://github.com/zhangfisher/voerka-i18n/)** +**[`VoerkaI18n`](https://github.com/zhangfisher/voerka-i18n/)** 适用于多框架的 JavaScript 国际化解决方案,提供对 openInula 的适配。 From 3bebf5280ee956854bee4d0288d113d1779cbc1f Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Mon, 25 Dec 2023 11:36:02 +0800 Subject: [PATCH 107/108] =?UTF-8?q?[all]=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/inula-intl/package.json | 4 ++-- packages/inula-request/package.json | 4 ++-- packages/inula-router/package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/inula-intl/package.json b/packages/inula-intl/package.json index a55c28d1..f15665e3 100644 --- a/packages/inula-intl/package.json +++ b/packages/inula-intl/package.json @@ -1,6 +1,6 @@ { "name": "inula-intl", - "version": "0.0.4", + "version": "0.0.5", "description": "", "main": "build/intl.umd.js", "type": "commonjs", @@ -24,7 +24,7 @@ "author": "", "license": "MulanPSL2", "peerDependencies": { - "openinula": "^0.1.1" + "openinula": ">=0.1.1" }, "devDependencies": { "@babel/core": "7.21.3", diff --git a/packages/inula-request/package.json b/packages/inula-request/package.json index 2964a78e..beac309b 100644 --- a/packages/inula-request/package.json +++ b/packages/inula-request/package.json @@ -1,6 +1,6 @@ { "name": "inula-request", - "version": "0.0.7", + "version": "0.0.9", "description": "Inula-request brings you a convenient request experience!", "main": "./dist/inulaRequest.js", "scripts": { @@ -62,7 +62,7 @@ "webpack-dev-server": "^4.13.3" }, "peerDependencies": { - "openinula": "^0.1.1" + "openinula": ">=0.1.1" }, "exclude": [ "node_modules" diff --git a/packages/inula-router/package.json b/packages/inula-router/package.json index 8bd78e8d..b2476ad7 100644 --- a/packages/inula-router/package.json +++ b/packages/inula-router/package.json @@ -1,6 +1,6 @@ { "name": "inula-router", - "version": "0.0.3", + "version": "0.0.4", "description": "router for inula framework, a part of inula-ecosystem", "main": "./router/cjs/router.js", "module": "./router/esm/router.js", From b026ca8786b1a27f494fff6eb1ebc33566d86e31 Mon Sep 17 00:00:00 2001 From: cgj <18700163154@163.com> Date: Thu, 28 Dec 2023 17:35:46 +0800 Subject: [PATCH 108/108] =?UTF-8?q?[comment]=E6=B7=BB=E5=8A=A0CONTRIBUTING?= =?UTF-8?q?.md=EF=BC=8C=E5=8C=85=E5=90=AB=E4=B8=80=E4=BA=9B=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E7=9B=B8=E5=85=B3=E7=9A=84=E5=BB=BA=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..46d6888f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,53 @@ +## 代码注释 + +为提高代码的可读性,我们希望你能按照以下几条规则编写注释, + +- 核心:```不要解释是什么,而是回答为什么``` + +- 统一的注释风格 + + ``` tsx + /** + * 这是块级注释 + */ + + // 这是行内注释 + + //============================== 分隔符 ============================== + ``` + +- 注释不要重复代码内容 + + ``` tsx + // 注释和代码含义相同,无需添加 + // error是个promise + if (isPromise(error)) { + ... + } + ``` + +- 注释不要解释模糊代码 + + ``` tsx + // 最佳节点 + let n: Node | null = null; + ``` + + 直接在代码中通过名称表达清楚,可以省略掉注释 + + ``` tsx + let bestNode: Node | null = null; + ``` + + 如果添加注释则是补充性的额外信息 + + ```tsx + // 全局指针,在遍历时指向当前最佳的节点 + let bestNode: VNode | null = null; + ``` + +- 巧妙或复杂的代码要添加注释解释逻辑 + +- 对所有导出的顶层模块进行注释 + +- 注释应该是直接的,不要有不明确的表达或符号