chore: fix conflict

This commit is contained in:
haiqin 2024-01-08 11:53:10 +08:00
commit c9bb4c07de
165 changed files with 9089 additions and 413 deletions

View File

@ -1,17 +1,8 @@
---
name: 模板名称在新建pr时候能看到
about: 模板描述对应的pr模板卡片展示时候能看到介绍模板
---
**PR 描述:** [请描述提交此 PR 的背景、目的、所做的更改以及如何测试此 PR]
**关联的 Issues:** [请列出与此 PR 相关的 issue 编号]
**TODO可选**
- [ ] 任务1
- [ ] ...
**检查项:**
**检查项(无需修改,提交后界面上可勾选):**
- [ ] 代码已经被检视
- [ ] 代码符合项目的代码标准和最佳实践
- [ ] 代码已经通过所有测试用例

View File

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

53
CONTRIBUTING.md Normal file
View File

@ -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;
```
- 巧妙或复杂的代码要添加注释解释逻辑
- 对所有导出的顶层模块进行注释
- 注释应该是直接的,不要有不明确的表达或符号

View File

@ -6,75 +6,82 @@
## 技术架构
![输入图片说明](https://gitee.com/openInula/inula-docs/raw/master/static/img/structure.png)
![](https://openinula-website.obs.ap-southeast-1.myhuaweicloud.com/misc/structure.png)
### 核心能力
## 核心能力
**响应式API**
### 响应式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 保持一致 API 的特性、可以无缝支持 React 生态。
* 使用传统 API 可以无缝将 React 项目切换至 openInulaReact 应用可零修改切换至 openInula。
### 兼容 React API
提供与 React 一致的 API完全支持 React 生态,可将 React 应用可零修改切换至 openInula。
### openInula 配套组件
**状态管理器/inula-X**
#### 状态管理器 inula-X
inula-X 是 openInula 默认提供的状态管理器,无需额外引入三方库,就可以简单实现跨组件/页面共享状态。
inula-X 与 Redux 比可创建多个 Store不需要在 Reducer 中返回 state 并且简化了 Action 和 Reducer 的创建步骤原生支持异步能力组件能做到精准重渲染。inula-X 均可使用函数组件、class 组件,能提供 redux 的适配接口及支持响应式的特点。
inula-X 是 openInula 默认提供的状态管理器。无需额外引入三方库,就可以简单实现跨组件/页面共享状态。
**路由/inula-router**
inula-X 与 Redux 相比,可创建多个 Store不需要在 Reducer 中返回 state 并且简化了 Action 和 Reducer 的创建步骤原生支持异步能力组件能做到精准重渲染。inula-X 均可使用函数组件、class 组件,能提供 redux 的适配接口及支持响应式的特点。
inula-router 是 openInula 生态组建的一部分,为 openInula 提供前端路由的能力,是构建大型应用必要组件。
inula-router 涵盖 react-router、history、connect-react-router 的功能。
#### 路由 inula-router
**请求/inula-request**
inula-router 为 openInula 提供前端路由的能力,是构建大型应用必要组件,涵盖 react-router、history、connect-react-router 的功能。
inula-request 是 openInula 生态组件,涵盖常见的网络请求方式,并提供动态轮询钩子函数给用户更便捷的定制化请求体验。
#### 请求 inula-request
**国际化/inula-intl**
inula-request 是 openInula 的网络请求组件,不仅涵盖常见的网络请求方式,还提供动态轮询钩子函数给用户更便捷的定制化请求体验。
inula-intl 是基于 openInula 生态组件,其主要提供了国际化功能,涵盖了基本的国际化组件和钩子函数,便于用户在构建国际化能力时方便操作。
#### 国际化 inula-intl
**调试工具/inula-dev-tools**
inula-intl 是基于 openInula 的国际化组件,涵盖了基本的国际化组件和钩子函数,允许用户更方便地构建国际化能力。
#### 调试工具 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等功能开箱即用。
**参与贡献**
欢迎您参与贡献,我们鼓励开发者以各种方式参与文档反馈和贡献。
**[`VoerkaI18n`](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)
## 许可协议
openInula 主要遵循 Mulan Permissive Software License v2 协议,详情请参考各代码仓 LICENSE 声明。
openInula 主要遵循 [Mulan Permissive Software License v2](http://license.coscl.org.cn/MulanPSL2) 协议,详情请参考各代码仓 LICENSE 声明。
## 联系方式
team@inulajs.org
* 官方邮箱: [team@inulajs.org](mailto:team@inulajs.org)
* 微信公众号:
![](https://www.openinula.net/assets/qrcode.inula-02f99d58.jpg)

View File

@ -1,3 +1,28 @@
# 0.0.2 版本
## 新特性
- **inula-request** 新增响应体中获取完整 URL 能力。
## API变更
## Bug修复
- **inula** 解决事件卸载失败问题。
- **inula** 解决 mouseover 重复触发 mouseEnter 事件问题。
- **inula** 大数组合并使用 concat。
- **inula** 事件支持 defaultPrevented 属性
## CVE漏洞修复
## 已知问题
# 0.0.1 版本
## 新特性

View File

@ -1,5 +1,5 @@
{
"name": "openinula",
"name": "inula",
"description": "OpenInula is a JavaScript framework library.",
"version": "0.0.1",
"private": true,

View File

@ -128,9 +128,9 @@ class BasicGenerator extends Generator {
if (fs.existsSync(fullpath)) {
this.traverseDirCapture(fullpath, dirCallback, fileCallback);
}
continue;
} else {
fileCallback(fullpath);
}
fileCallback(fullpath);
}
}
@ -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);
}
}
}

View File

@ -0,0 +1,4 @@
# openinula + vite
该模板提供了 `openinula` 工作在 `vite`的基础配置。
> 请注意由于Vite插件有node版本限制请使用`node -v`命令确认node版本大于等于node v18。

View File

@ -11,7 +11,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"openinula": "^0.0.11"
"openinula": "^0.1.1"
},
"devDependencies": {
"@babel/core": "^7.21.4",

View File

@ -10,7 +10,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"openinula": "^0.0.11"
"openinula": "^0.1.1"
},
"devDependencies": {
"@babel/core": "^7.21.4",

View File

@ -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',
@ -78,4 +78,7 @@ module.exports = {
port: 9000,
open: true,
},
resolve: {
extensions: ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'],
},
};

View File

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

View File

@ -0,0 +1,3 @@
{
"description": "simple reactive app template."
}

View File

@ -0,0 +1,4 @@
# openinula + vite
该模板提供了 `openinula` 工作在 `vite`的基础配置。
> 请注意由于Vite插件有node版本限制请使用`node -v`命令确认node版本大于等于node v18。

View File

@ -0,0 +1,11 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>My Inula App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>

View File

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

View File

@ -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 (
<div>
<div>{countText}</div>
<div>组件渲染次数:{renderCount}</div>
</div>
);
}
export default ReactiveComponent;

View File

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

View File

@ -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 (
<div class="container">
<div class="hero">
<h1 class="hero-title animate__animated animate__bounceInDown">欢迎来到 Inula 项目!</h1>
<p class="hero-subtitle animate__animated animate__bounceInUp">你已成功创建你的第一个响应式 Inula 项目</p>
</div>
<div className="content">
<div className="card animate__animated animate__zoomIn">
<ReactiveComponent/>
</div>
</div>
<div class="content">
<div class="card animate__animated animate__zoomIn">
<h2>了解更多</h2>
<p>
要了解 Inula查看{' '}
<a href="https://openinula.com/" target="_blank">Inula 官网</a>
</p>
</div>
</div>
</div>
);
}
Inula.render(<App />, document.getElementById('root'));

View File

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

View File

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

View File

@ -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 (
<div class="container">
<div class="hero">
<h1 class="hero-title animate__animated animate__bounceInDown">欢迎来到 Inula 项目!</h1>
<p class="hero-subtitle animate__animated animate__bounceInUp">你已成功创建你的第一个响应式 Inula 项目</p>
</div>
<div className="content">
<div className="card animate__animated animate__zoomIn">
<ReactiveComponent/>
</div>
</div>
<div class="content">
<div class="card animate__animated animate__zoomIn">
<h2>了解更多</h2>
<p>
要了解 Inula查看{' '}
<a href="https://openinula.org" target="_blank">Inula 官网</a>
</p>
</div>
</div>
</div>
);
}
}
export default App;

View File

@ -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 (
<div>
<div>{countText}</div>
<div>组件渲染次数:{renderCount}</div>
</div>
);
}
export default ReactiveComponent;

View File

@ -0,0 +1,11 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Inula App</title>
</head>
<body>
<div id="root"></div>
<script src="../dist/bundle.js"></script>
</body>
</html>

View File

@ -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(<App />, document.getElementById('root'));

View File

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

View File

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

View File

@ -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([
{

View File

@ -1,11 +1,14 @@
{
"name": "create-inula",
"version": "0.0.2",
"version": "0.0.8",
"description": "",
"main": "index.js",
"bin": {
"create-inula": "bin/cli.js"
},
"engines": {
"node": ">= 18.0.0"
},
"files": [
"bin",
"lib",

View File

@ -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 {
}
}
```

View File

@ -165,4 +165,3 @@ export interface Arguments {
'--'?: Array<string | number>;
[argName: string]: any;
}

View File

View File

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

19
packages/inula-dev-tools/externals.d.ts vendored Normal file
View File

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

21
packages/inula-dev-tools/global.d.ts vendored Normal file
View File

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

View File

@ -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.1.1",
"flatted-object": "^0.1.2",
"json-decycle": "^2.0.1",
"lodash": "^4.17.21",
"object-assign": "^4.1.1"
}
}

View File

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

View File

@ -0,0 +1,296 @@
/*
* 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;
}
}
}
});

View File

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

View File

@ -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<IAttr>} editableAttrs props hooks
* @param {number} index editableAttrs
* @param {string} attrsType props hooks
* @return {Array} vNode
*/
const getPath = (editableAttrs: IAttr[], index: number, attrsType: string): Array<string | number> => {
const path: Array<string | number> = [];
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(
<div
className={styles.info}
style={{ paddingLeft: item.indentation * 10 }}
key={index}
onclick={() => handleCollapse(item)}
>
<span className={styles.attrArrow}>{hasChild && <Triangle director={isCollapsed ? 'right' : 'down'} />}</span>
<span className={styles.attrName}>{`${item.name}`}</span>
<div className={styles.colon}>{':'}</div>
{item.type === 'string' || item.type === 'number' || item.type === 'undefined' || item.type === 'null' ? (
<>
<input
value={getShowName(item.value)}
data-type={item.type}
className={styles.attrValue}
onChange={event => {
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);
}
}
}}
/>
<div className={styles.operation} ref={operationRef}>
<span className={styles.operationIcon} onclick={event => operationClick(event, operationRef)}>
<Operation />
</span>
</div>
</>
) : item.type === 'boolean' ? (
<>
<span data-type={item.type} className={styles.attrValue}>
{item.value.toString()}
</span>
<input
type={'checkbox'}
checked={item.value}
className={styles.checkBox}
onChange={event => {
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);
}
}}
/>
</>
) : (
<>
<span data-type={item.type} className={styles.attrValue}>
{item.value}
</span>
<div className={styles.operation} ref={operationRef}>
<span className={styles.operationIcon} onClick={event => operationClick(event, operationRef)}>
<Operation />
</span>
</div>
</>
)}
</div>
);
if (isCollapsed) {
currentIndentation = indentation;
}
});
return (
<div className={styles.attrContainer}>
<div className={styles.attrHead}>
<span className={styles.attrType}>{attrsName}</span>
</div>
<div className={styles.attrDetail}>{showAttr}</div>
</div>
);
});
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 | HTMLElement>(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<string | number>) => {
postMessageToBackground(CopyToConsole, { id, itemName, attrsName, path });
dropdownRef.current.classList.toggle(styles['active']);
};
const storeVariable = (attrsName: string, path: Array<string | number>) => {
postMessageToBackground(StorageValue, { id, attrsName, path });
dropdownRef.current.classList.toggle(styles['active']);
};
return (
<div className={styles.infoContainer}>
<div className={styles.componentInfoHead}>
{name && (
<>
<div className={styles.name}>
<div className={styles.text}>{name}</div>
</div>
<button className={styles.button}>
<span
className={styles.eye}
title={'Inspect dom element'}
onClick={() => {
doInspectDom(id);
}}
>
<Eye />
</span>
</button>
<button className={styles.button} disabled={false}>
<span
className={styles.location}
onClick={() => {
doViewSource(id);
}}
title={'View source for this element'}
>
<Location />
</span>
</button>
<button className={styles.button}>
<span
className={styles.debug}
title={'Log this component data'}
onClick={() => {
postMessageToBackground(LogComponentData, id);
}}
>
<Debug />
</span>
</button>
</>
)}
</div>
<div className={styles.componentInfoMain}>
{Object.keys(attrs).map(attrsType => {
const parsedAttrs = attrs[attrsType];
if (parsedAttrs && parsedAttrs.length !== 0) {
const attrsName = attrsType.slice(6); // parsedState => State
return (
<ComponentAttr
attrsName={attrsName}
attrsType={attrsType}
attrs={parsedAttrs}
id={id}
dropdownRef={dropdownRef.current}
/>
);
}
return null;
})}
<div className={styles.parentsInfo}>
{name && (
<div>
<div className={styles.parentName}>Parents</div>
{parents.map(item => (
<button className={styles.parent} onClick={() => onClickParent(item)}>
{`<${item.name.itemName}>`}
</button>
))}
</div>
)}
</div>
<div className={styles.parentsInfo}>
{source && (
<>
<div>source: {''}</div>
<div style={{ marginLeft: '1rem' }}>{sourceFormatted(source.fileName, source.lineNumber)}</div>
</>
)}
</div>
<div ref={dropdownRef} className={styles.dropdown}>
<ul>
<li
onClick={() =>
copyToConsole(
(dropdownRef.current as any).attrInfo.itemName,
(dropdownRef.current as any).attrInfo.attrsName,
(dropdownRef.current as any).attrInfo.path
)
}
>
<b>Copy value to console</b>
</li>
<li
onClick={() => storeVariable((dropdownRef.current as any).attrInfo.attrsName, (dropdownRef.current as any).attrInfo.path)}
>
<b>Store as global variable</b>
</li>
</ul>
</div>
</div>
</div>
);
}
export default memo(ComponentInfo);

View File

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

View File

@ -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 (
<input
onkeyup={handleKeyUp}
onchange={handleChange}
className={styles.search}
value={value}
placeholder='Search Component'
/>
);
}

View File

@ -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<HTMLDivElement>();
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 (
<div ref={containerRef} {...rest}>
{myChild}
</div>
);
}

View File

@ -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<T> {
// 不要用 indexOf 进行位置计算,它会遍历数组
private lastRenderItemToIndexMap: Map<T | undefined, number>;
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 | undefined)[] = [];
const length = nextItems.length;
const nextRenderItemToIndexMap = new Map<T | undefined, number>();
const addItems: T[] = [];
// 遍历 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 中存在空 indexnextItems 已经耗尽,不用处理
// 确保新旧数组中 item 的 index 值不会发生变化
this.lastRenderItemToIndexMap = nextRenderItemToIndexMap;
return nextRenderItems;
}
}

View File

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

View File

@ -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<T extends { id: number | string }> {
data: T[];
maxDeep: number;
width: number; // 暂时未用到,当需要支持横向滚动时使用
height: number; // VList 的高度
children?: any; // inula 组件
itemHeight: number;
scrollToItem?: T; // 滚动到指定项位置,如果该项在可见区域内,不滚动,如果补在,则滚动到中间位置
onRendered: (renderInfo: RenderInfoType<T>) => void;
filter?: (data: T) => boolean; // false 表示该行不显示
}
export type RenderInfoType<T> = {
visibleItems: T[];
}
function parseTranslate<T>(data: T[], itemHeight: number) {
const map = new Map<T, number>();
data.forEach((item, index) => {
map.set(item, index * itemHeight);
})
return map;
}
export function VList<T extends { id: number | string }>(props: IProps<T>) {
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<T> } = useRef({
visibleItems: [],
});
const [indentationLength, setIndentationLength] = useState(0);
// 每个 item 的 translateY 值固定不变
const itemToTranslateYMap = useMemo(() => parseTranslate(data, itemHeight), [data]);
const itemIndexMap = useMemo(() => new ItemMap<T>(), []);
const containerRef = useRef<HTMLDivElement>();
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 (
<div
key={String(index)} // 固定 key 值,这样就只会更新 translateY 的值
className={styles.item}
style={{ transform: `translateY(${itemToTranslateYMap.get(item)}px)` }}
>
{children(item,indentationLength)}
</div>
);
});
return (
<div ref={containerRef} className={styles.container}>
{list}
<div style={{ marginTop: totalHeight }}></div>
</div>
);
}

View File

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

View File

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

View File

@ -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 ? <Triangle director={isCollapsed ? 'right' : 'down'} /> : '';
const handleClickCollapse = () => {
onCollapse(data);
};
const handleClick = () => {
onClick(data);
};
const handleMouseEnter = () => {
onMouseEnter(data);
};
const handleMouseLeave = () => {
onMouseLeave(data);
};
const itemAttr: Record<string, any> = {
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<any>, badgeName: string) => {
showName.push(' ');
showName.push(<div className={`${styles.Badge}`}>{badgeName}</div>);
};
const pushItemName = (showName: Array<any>, cutName: string, char: string) => {
const index = cutName.search(char);
if (index > -1) {
const notHighlightStr = cutName.slice(0, index);
showName.push(`<${notHighlightStr}`);
showName.push(<mark>{char}</mark>);
showName.push(`${cutName.slice(index + char.length)}>`);
} else {
showName.push(`<${cutName}`);
}
};
const pushBadgeName = (showName: Array<any>, cutName: string, char: string) => {
const index = cutName.search(char);
if (index > -1) {
const notHighlightStr = cutName.slice(0, index);
showName.push(
<div className={`${styles.Badge}`}>
{notHighlightStr}
{<mark>{char}</mark>}
{cutName.slice(index + char.length)}
</div>
);
} else {
pushBadge(showName, cutName);
}
};
const reg = createRegExp(highlightValue);
const heightCharacters = name.itemName.match(reg);
const showName = [];
const addShowName = (showName: Array<string>, name: NameObj) => {
showName.push(`<${name.itemName}>`);
name.badge.forEach(key => {
showName.push(<div className={`${styles.Badge}`}>{key}</div>);
});
};
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 (
<div {...itemAttr}>
<div
style={{marginLeft: indentation * indentationLength}}
className={styles.treeIcon}
onclick={handleClickCollapse}
>
{showIcon}
</div>
<span className={styles.componentName}>{showName}</span>
{isShowKey && (
<>
<span className={styles.componentKeyName}>&nbsp;key</span>
{'="'}
<span className={styles.componentKeyValue}>{userKey}</span>
{'"'}
</>
)}
</div>
);
}
function VTree(props: {
data: IData[];
maxDeep: number;
highlightValue: string;
scrollToItem: IData;
onRendered: (renderInfo: RenderInfoType<IData>) => 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<Array<IData>>([]);
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<IData> => {
const index = data.indexOf(item);
const childList: Array<IData> = [];
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 (
<SizeObserver className={styles.treeContainer}>
{(width: number, height: number) => {
return (
<VList
data={showList}
maxDeep={maxDeep}
width={width}
height={height}
itemHeight={17.5}
scrollToItem={selectItem}
onRendered={onRendered}
>
{(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 (
<Item
indentationLength={indentationLength}
hasChild={hasChild}
onCollapse={changeCollapseNode}
onClick={handleClickItem}
onMouseEnter={handleMouseEnterItem}
onMouseLeave={handleMouseLeaveItem}
isCollapsed={collapseNode.includes(item)}
isSelect={selectItem === item}
highlightValue={highlightValue}
data={item}
isSelectedItemChild={childItems.includes(item)}
/>
);
}}
</VList>
);
}}
</SizeObserver>
);
}
export default memo(VTree);

View File

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

View File

@ -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
*
* <div id='test>
* <object> --> div
* <html></html> --> resize
* </object>
* </div>
*
*/
function timeout(func) {
return setTimeout(func, 20);
}
function requestFrame(func) {
const raf = requestAnimationFrame || timeout;
return raf(func);
}
function cancelFrame(id) {
const cancel = cancelAnimationFrame || clearTimeout;
cancel(id);
}
// 在闲置帧触发回调事件,如果在本次触发前存在未处理回调事件,需要取消未处理回调事件
function resizeListener(event) {
const win = event.target;
if (win.__resizeRAF__) {
cancelFrame(win.__resizeRAF__);
}
win.__resizeRAF__ = requestFrame(function () {
const observeElement = win.__observeElement__;
observeElement.__resizeCallbacks__.forEach(function (func) {
func.call(observeElement, observeElement, event);
});
});
}
function loadObserver(this: any) {
// 将待观测元素传递给 object 标签的 window 对象,这样在触发 resize 事件时可以拿到待观测元素
this.contentDocument.defaultView.__observeElement__ = this.__observeElement__;
// 给 html 的 window 对象添加 resize 事件
this.contentDocument.defaultView.addEventListener('resize', resizeListener);
}
export function addResizeListener(element: any, func: any) {
if (!element.__resizeCallbacks__) {
element.__resizeCallbacks__ = [func];
element.style.position = 'relative';
const observer = document.createElement('object');
observer.setAttribute(
'style',
'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;'
);
observer.data = 'about:blank';
observer.onload = loadObserver;
observer.type = 'text/html';
observer['__observeElement__'] = element;
element.__observer__ = observer;
element.appendChild(observer);
} else {
element.__resizeCallbacks__.push(func);
}
}
export function removeResizeListener(element, func) {
element.__resizeCallbacks__.splice(element.__resizeCallbacks__.indexOf(func), 1);
if (!element.__resizeCallbacks__.length) {
element.__observer__.contentDocument.defaultView.removeEventListener('resize', resizeListener);
element.removeChild(element.__observer__);
element.__observer__ = null;
}
}

View File

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

View File

@ -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>): 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<OverlayRect>;
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<VNode>) {
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<VNode> | null) {
if (window.document == null || elements == null) {
return;
}
if (elementOverlay === null) {
elementOverlay = new ElementOverlay();
}
elementOverlay.execute(elements);
}

View File

@ -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<T extends BaseType>(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,
};
}

View File

@ -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<string | number>} path
* @param {string} attrsName props hooks
*/
const getValueByPath = (
vNode: VNode,
path: Array<string | number>,
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<string | number>} path
* @param {string} attrsName
*/
function logDataWithPath(
id: number,
itemName: string,
path: Array<string | number>,
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<string |number>} path
* @param {string} attrsName
*/
function storeDataWithPath(
id: number,
path: Array<string | number>,
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();
},
],
// 请求某个节点的 propshooks
[
RequestComponentAttrs,
data => {
parseCompAttrs(data);
},
],
// 修改 propshooks
[
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();

View File

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

View File

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

View File

@ -0,0 +1,30 @@
<!--
~ 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.
-->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src *; style-src 'self' 'unsafe-inline'; srcipt-src 'self' 'unsafe-inline' 'unsafe-eval' ">
<script src="inula.development.js"></script>
</head>
<body>
<div>
<p>Inula dev tools!</p>
</div>
</body>
<script> src="main.js"</script>
</html>

View File

@ -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": [
"<all_urls>"
],
"js": [
"contentScript.js"
],
"run_at": "document_start"
}
],
"web_accessible_resources": [
"injector.js",
"background.js"
]
}

View File

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

View File

@ -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 的 nameuserKeyindentation 属性不会发生变化
// 但是在跳转到新页面时, 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<IData>(null);
const [isPicking, setPicking] = useState(false);
const [source, setSource] = useState<Source>(null);
const idToTreeNodeMapref = useRef<IIdToNodeMap>({});
const [state, dispatch] = useReducer(
resizeReducer,
null,
initResizeState
);
const pageRef = useRef<null | HTMLElement>(null);
const treeRef = useRef<null | HTMLElement>(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 (
<ViewSourceContext.Provider value={{ viewSourceFunction }}>
<PickElementContext.Provider value={{ pickElementFunction }}>
<div
ref={pageRef}
onMouseMove={onResize}
onMouseLeave={stopResize}
onMouseUp={stopResize}
className={styles.app}
>
<div ref={treeRef} className={styles.left}>
<div className={styles.leftTop}>
<div className={styles.select}>
<button className={`${pickClassName}`}>
<span
className={styles.eye}
title={'Pick an element from the page'}
onClick={() => {
postMessageToBackground(!isPicking ? PickElement : StopPickElement);
setPicking(!isPicking);
}}
>
<Select />
</span>
</button>
</div>
<div className={styles.divider} />
<div className={styles.search}>
<Discover />
<Search onKeyUp={onSelectNext} onChange={handleSearchChange} value={filterValue} />
</div>
{filterValue !== '' && (
<>
<span className={styles.searchResult}>
{`${matchItems.indexOf(currentItem) + 1}/${matchItems.length}`}
</span>
<div className={styles.divider} />
<button
className={styles.searchAction}
onClick={onSelectLast}
>
<Arrow direction={'up'} />
</button>
<button
className={styles.searchAction}
onClick={onSelectNext}
>
<Arrow direction={'down'} />
</button>
<button className={styles.searchAction} onClick={onClear}>
<Close />
</button>
</>
)}
</div>
<div>
<VTree
data={parsedVNodeData}
maxDeep={maxDeep}
highlightValue={filterValue}
onRendered={onRendered}
collapsedNodes={collapsedNodes}
onCollapseNode={setCollapsedNodes}
scrollToItem={currentItem}
selectItem={selectComp}
onSelectItem={handleSelectComp}
/>
</div>
</div>
<div>
<div onMouseDown={doResize} className={styles.resizeLine} />
</div>
<div>
<ComponentInfo
name={selectComp ? selectComp.name.itemName : null}
attrs={selectComp ? componentAttrs : {}}
parents={parents}
id={selectComp ? selectComp.id : null}
source={selectComp ? source : null}
onClickParent={handleClickParent}
/>
</div>
</div>
</PickElementContext.Provider>
</ViewSourceContext.Provider>
);
}
export default memo(Panel);

View File

@ -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 Panel from './Panel';
// 这里导出 Panel 为了加载 Panel.less
export default Panel;

View File

@ -0,0 +1,35 @@
<!doctype html>
<html>
<head>
<meta charset="utf8">
<meta http-equiv="Content-Security-Policy"
content="default-src *; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' ">
<style>
html {
width: 100%;
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow-y: hidden;
}
#root {
width: 100%;
height: 100%;
}
</style>
<script src="inula.development.js"></script>
</head>
<body>
<div id="root"></div>
<script src="panel.js"></script>
</body>
</html>

View File

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

View File

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

View File

@ -0,0 +1,125 @@
/*
* 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, { useState } from 'openinula';
import { Modal } from './Modal';
import { highlight, sendMessage } from './utils';
function executeAction(storeId: string, name: string, args: any[]) {
sendMessage({
type: 'inulax run action',
tabId: chrome.devtools.inspectedWindow.tabId,
storeId,
action: name,
args,
});
}
function queryAction(storeId: string, name: string, args: any[]) {
sendMessage({
type: 'inulax queue action',
tabId: chrome.devtools.inspectedWindow.tabId,
storeId,
action: name,
args,
});
}
export function ActionRunner({ foo, storeId, actionName }) {
const [data, setState] = useState({
modal: false,
gatheredAttrs: [],
query: false,
});
const modalIsOpen = data.modal;
const gatheredAttrs = data.gatheredAttrs;
function setData(val) {
const newData = {
modal: data.modal,
gatheredAttrs: data.gatheredAttrs,
};
Object.entries(val).forEach(([key, value]) => (newData[key] = value));
setState(newData as any);
}
const plainFunction = foo.replace(/\{.*}/gms, '');
const attributes = plainFunction
.replace(/^.*\(/g, '')
.replace(/\).*$/, '')
.split(/, ?/)
.filter((item, index) => index > 0);
return (
<>
<span
title={'Run action'}
onClick={() => {
if (attributes.length > 0) {
setData({ modal: false, gatheredAttrs: [], query: false });
} else {
executeAction(storeId, actionName, gatheredAttrs);
}
}}
>
<b
style={{
cursor: 'pointer',
}}
>
<span
title={'Add to action queue'}
onClick={e => {
e.preventDefault();
if (attributes.len > 0) {
setData({ modal: true, gatheredAttrs: [], query: true });
} else {
queryAction(storeId, actionName, gatheredAttrs);
}
}}
>
{' '}
</span>
</b>
<span>
<i>{plainFunction}</i>
{' {...}'}
</span>
</span>
{modalIsOpen ? (
<Modal
closeModal={() => {
setData({ modal: false });
}}
then={data => {
if (gatheredAttrs.length === attributes.length - 1) {
setData({ modal: false });
executeAction(storeId, actionName, gatheredAttrs.concat(data));
} else {
setData({
gatheredAttrs: gatheredAttrs.concat([data]),
});
}
}}
>
<h3>{data.query ? 'Query action:' : 'Run action:'}</h3>
<p>{highlight(plainFunction, attributes[gatheredAttrs.length])}</p>
</Modal>
) : null}
</>
);
}

View File

@ -0,0 +1,335 @@
/*
* 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 { Tree } from './Tree';
import {displayValue, omit} from './utils';
type Mutation = {
mutation: boolean;
items?: Mutation[];
attributes?: { [key: string]: Mutation };
values?: Mutation[];
entries?: Mutation[];
from?: any;
to?: any;
};
export function DiffTree({
mutation,
indent = 0,
index = '',
expand = false,
search = '',
forcedExpand = false,
omitAttrs = [],
doNotDisplayIcon = false,
forcedLabel = null,
className,
}: {
mutation: Mutation;
indent: number;
index?: string | number;
expand?: boolean;
search: string;
forcedExpand?: boolean;
omitAttrs: string[];
doNotDisplayIcon?: boolean;
forcedLabel?: string | number | null;
className?: string;
}) {
if (omitAttrs.length && mutation.attributes) {
mutation.attributes = omit(mutation.attributes, ...omitAttrs);
mutation.from = mutation.from && omit(mutation.from, ...omitAttrs);
mutation.to = mutation.to && omit(mutation.to, ...omitAttrs);
}
const [expanded, setExpanded] = useState(expand);
const deleted = mutation.mutation && !('to' in mutation);
const newValue = mutation.mutation && !('from' in mutation);
const mutated = mutation.mutation;
const isArray = mutated && mutation.items;
const isObject = mutated && mutation.attributes;
const isMap = mutated && mutation.entries;
const isSet = mutated && mutation.values;
const isPrimitive = !isArray && !isObject && !isMap && !isSet;
if (!mutated) {
return (
<Tree
data={mutation.to}
indent={indent}
search={search}
expand={expand}
forcedExpand={forcedExpand}
omitAttrs={omitAttrs}
forcedLabel={forcedLabel}
/>
);
}
if (newValue) {
return (
<Tree
data={mutation.to}
indent={indent}
search={search}
expand={expand}
forcedExpand={forcedExpand}
className={styles.added}
omitAttrs={omitAttrs}
forcedLabel={forcedLabel}
/>
);
}
if (deleted) {
return (
<Tree
data={mutation.from}
indent={indent}
search={search}
expand={expand}
forcedExpand={forcedExpand}
className={styles.deleted}
omitAttrs={omitAttrs}
forcedLabel={forcedLabel}
/>
);
}
return (
<div
style={{
fontFamily: 'monospace',
}}
className={`${
expanded
? 'expanded'
: `not-expanded ${
mutated && !isPrimitive && !expanded ? styles.changed : ''
}`
}`}
onClick={e => {
e.stopPropagation();
}}
>
<span
style={{
cursor: 'pointer',
}}
onClick={() => {
setExpanded(!expanded);
}}
>
{new Array(Math.max(indent, 0)).fill(<span>&nbsp;</span>)}
{isPrimitive ? (
// 如果两个 value 是基本变量并且不同,则简单显示不同点
<div
onClick={e => {
e.stopPropagation();
}}
>
<Tree
data={mutation.from}
indent={indent}
search={search}
index={index}
className={styles.deleted}
omitAttrs={omitAttrs}
/>
<Tree
data={mutation.to}
indent={indent}
search={search}
index={index}
className={styles.added}
omitAttrs={omitAttrs}
/>
</div>
) : (
// 如果至少有一个是复杂变量,则需要展开按钮
<>
{forcedExpand ? '' : expanded ? <span></span> : <span></span>}
{index === 0 || index ? (
<b className={styles.purple}>{displayValue(index, search)}: </b>
) : (
''
)}
{isArray ? (
// 如果都是数组进行比较
expanded ? (
[
Array(Math.max(mutation.from.length, mutation.to.length))
.fill(true)
.map((i, index) => {
return (
<div>
{mutation.items[index].mutation ? (
<DiffTree
mutation={{
...mutation.items[index],
to: mutation.to[index],
}}
indent={indent}
search={search}
omitAttrs={omitAttrs}
forcedLabel={index}
/>
) : (
<Tree
data={mutation.to[index]}
indent={indent}
search={search}
index={index}
className={styles.default}
omitAttrs={omitAttrs}
/>
)}
</div>
);
}),
]
) : (
forcedLabel || `Array(${mutation.to?.length})`
)
) : isSet ? (
expanded ? (
<div>
<div>
{forcedLabel || `Set(${mutation.to?.values.length})`}
</div>
{Array(
Math.max(
mutation.from?.values.length,
mutation.to?.values.length
)
)
.fill(true)
.map((i ,index) => (
<div>
{mutation.values[index].mutation ? (
<DiffTree
mutation={{
...mutation.values[index],
}}
indent={indent + 2}
search={search}
omitAttrs={omitAttrs}
/>
) : (
<Tree
data={mutation.to?.values[index]}
indent={indent + 2}
search={search}
className={styles.default}
omitAttrs={omitAttrs}
/>
)}
</div>
))}
</div>
) : (
<span>
{forcedLabel || `Set(${mutation.to?.values.length})`}
</span>
)
) : isMap ? (
expanded ? (
<>
<span>
{forcedLabel || `Map(${mutation.to?.entries.length})`}
</span>
{Array(
Math.max(
mutation.from?.entries.length,
mutation.to?.entries.length
)
)
.fill(true)
.map((i, index) =>
mutation.entries[index].mutation ? (
<div>
<DiffTree
mutation={{
...mutation.entries[index],
}}
indent={indent + 2}
search={search}
omitAttrs={omitAttrs}
forcedLabel={'[map item]'}
/>
</div>
) : (
<div>
<Tree
data={mutation.to?.entries[index]}
indent={indent + 2}
search={search}
className={styles.default}
omitAttrs={omitAttrs}
forcedLabel={'[map item]'}
/>
</div>
)
)}
</>
) : (
<span>
{forcedLabel || `Map(${mutation.to?.entries.length})`}
</span>
)
) : expanded ? (
// 如果都是 object 进行比较
Object.entries(mutation.attributes).map(([key, item]) => {
return item.mutation ? (
<span onClick={e => e.stopPropagation()}>
{
<DiffTree
mutation={item}
index={key}
indent={indent}
search={search}
className={!expanded && mutated ? '' : styles.changed}
omitAttrs={omitAttrs}
/>
}
</span>
) : (
<span onClick={e => e.stopPropagation()}>
{
<Tree
data={mutation.to[key]}
index={key}
indent={indent}
search={search}
className={styles.default}
omitAttrs={omitAttrs}
/>
}
</span>
);
})
) : (
forcedLabel || '{ ... }'
)}
</>
)}
</span>
</div>
);
}

View File

@ -0,0 +1,402 @@
/*
* 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 { DevToolPanel } from '../utils/constants';
import {
initBackgroundConnection,
addBackgroundMessageListener,
removeBackgroundMessageListener,
} from '../panelConnection';
import { Table } from './Table';
import { Tree } from './Tree';
import {fullTextSearch, omit} from './utils';
import styles from './PanelX.less';
import { Checkbox } from '../utils/Checkbox';
import { DiffTree } from './DiffTree';
const eventTypes = {
INITIALIZED: 'inulax store initialized',
STATE_CHANGE: 'inulax state change',
SUBSCRIBED: 'inulax subscribed',
UNSUBSCRIBED: 'inulax unsubscribed',
ACTION: 'inulax action',
ACTION_QUEUED: 'inulax action queued',
QUEUE_PENDING: 'inulax queue pending',
QUEUE_FINISHED: 'inulax queue finished',
};
const otherTypes = {
GET_EVENTS: 'inulax getEvents',
GET_PERSISTENCE: 'inulax getPersistence',
EVENTS: 'inulax events',
FLUSH_EVENTS: 'inulax flush events',
SET_PERSISTENT: 'inulax setPersistent',
RESET_EVENTS: 'inulax resetEvents',
};
function extractDataByType(message, search) {
if (message.type === eventTypes.ACTION) {
return (
<div
onClick={e => {
e.stopPropagation();
}}
>
<Tree
data={{
Action: `${message.data.action.action}${
message.data.fromQueue ? ' (queued)' : ''
}`
}}
expand={true}
indent={-4}
forcedExpand={true}
search={search}
omitAttrs={['_inulaObserver']}
/>
</div>
);
}
if (message.type === eventTypes.STATE_CHANGE) {
return (
<div
onClick={e => {
e.stopPropagation();
}}
>
<b>{`${message.data.change.vNodes.length} nodes changed:`}</b>
<Tree
data={message.data.change.vNodes.map(vNode => {
return (
<span>
<i>{vNode.type}</i>()
</span>
);
})}
/>
</div>
);
}
return <span className={styles.grey}>N/A</span>
}
export default function EventLog({ setNextStore, setEventFilter, eventFilter }) {
const [log, setLog] = useState([]);
const [initlized, setInitlized] = useState(false);
const [persistent, setPersistent] = useState(false);
const filterField = useRef(null);
const addFilter = (key, value) => {
const filters = { ...eventFilter };
filters[key] = value;
setEventFilter(filters);
};
const removeFilter = key => {
const filters = { ...eventFilter };
delete filters[key];
setEventFilter(filters);
};
if (!initlized) {
setTimeout(() => {
chrome.runtime.sendMessage({
type: 'INULA_DEV_TOOLS',
payload: {
type: otherTypes.GET_EVENTS,
tabId: chrome.devtools.inspectedWindow.tabId,
},
from: DevToolPanel,
});
chrome.runtime.sendMessage({
type: 'INULA_DEV_TOOLS',
payload: {
type: otherTypes.GET_PERSISTENCE,
tabId: chrome.devtools.inspectedWindow.tabId,
},
from: DevToolPanel,
});
}, 100);
}
useEffect(() => {
const lisener = message => {
if (message.payload.type.startsWith('inulax')) {
if (message.payload.type === otherTypes.EVENTS) {
setLog(message.payload.events);
setInitlized(true);
} else if (message.payload.type === otherTypes.SET_PERSISTENT) {
setPersistent(message.payload.persistent);
} else if (message.payload.type === otherTypes.FLUSH_EVENTS) {
chrome.runtime.sendMessage({
type: 'INULA_DEV_TOOLS',
payload: {
type: otherTypes.GET_EVENTS,
tabId: chrome.devtools.inspectedWindow.tabId,
},
from: DevToolPanel,
});
}
}
};
initBackgroundConnection('panel');
addBackgroundMessageListener(lisener);
return () => {
removeBackgroundMessageListener(lisener);
};
});
const filters = Object.entries(eventFilter);
const usedTypes = { all: 0 };
const processedData = log
.filter(event => {
if (!Object.values(eventTypes).includes(event.message.type)) {
return false;
}
usedTypes.all++;
if (!usedTypes[event.message.type]) {
usedTypes[event.message.type] = 1;
} else {
usedTypes[event.message.type]++;
}
if (!filters.length) {
return true;
}
return !filters.some(([key, value]) => {
if (key === 'fulltext') {
const result = fullTextSearch(event, value);
return !result;
}
const keys = key.split('.');
let search = event;
keys.forEach(attr => {
search = search[attr];
});
return value !== search;
});
})
.map(event => {
const date = new Date(event.timestamp);
return {
id: event.id,
timestamp: event.timestamp,
type: event.message.type,
time: `${date.toLocaleTimeString()} - ${date.toLocaleDateString()}`,
state: event.message.type === eventTypes.STATE_CHANGE ? (
<DiffTree
mutation={event.message.data.change.mutation}
expand={true}
forcedExpand={true}
indent={0}
search={eventFilter['fulltext']}
omitAttrs={['_inulaObserver']}
doNotDisplayIcon={true}
/>
) : (
<Tree
data={event.message.data.store.$s}
expand={true}
search={eventFilter['fulltext']}
forcedExpand={true}
indent={-4}
omitAttrs={['_inulaObserver']}
/>
),
storeClick: (
<span
className={styles.link}
onClick={e => {
e.preventDefault();
setNextStore(event.message.data.store.id);
}}
>
{event.message.data.store.id}
</span>
),
additionalData: extractDataByType(
event.message,
eventFilter['fulltext']
),
storeId: event.message.data.store.id,
event,
};
});
return (
<div>
<div style={{ marginTop: '0px', margin: '5px' }}>
<input
ref={filterField}
type={'text'}
placeholder={'Filter:'}
className={`${styles.compositeInput} ${styles.left}`}
onKeyUp={() => {
if (!filterField.current.value) {
removeFilter('fulltext');
}
addFilter('fulltext', filterField.current.value);
}}
/>
<button
className={`${styles.bold} ${styles.compositeInput} ${styles.right}`}
onClick={() => {
filterField.current.value = '';
removeFilter('fulltext');
}}
>
X
</button>
<span className={styles.grey}>{' | '}</span>
<span
style={{
cursor: 'pointer'
}}
onClick={e => {
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);
}}
>
<Checkbox value={persistent}></Checkbox> Persistent events
</span>
{' | '}
<button
onClick={() => {
// 重置 events
chrome.runtime.sendMessage({
type: 'INULA_DEV_TOOLS',
payload: {
type: otherTypes.RESET_EVENTS,
tabId: chrome.devtools.inspectedWindow.tabId,
},
from: DevToolPanel,
});
}}
>
Reset
</button>
{eventFilter['message.data.store.id'] ? (
<span>
{' | '}
<b
style={{
cursor: 'pointer',
}}
onClick={() => {
setNextStore(eventFilter['message.data.store.id']);
}}
>{` Displaying: [${eventFilter['message.data.store.id']}] `}</b>
<button
onClick={() => {
removeFilter('message.data.store.id');
}}
>
X
</button>
</span>
) : null}
</div>
<div style={{ marginTop: '0px', margin: '5px' }}>
<button
className={`${styles.filterButton} ${log.length ? '' : styles.grey} ${
eventFilter['message.type'] ? '' : styles.active
}`}
onClick={() => {
removeFilter('message.type');
}}
>
All({usedTypes.all})
</button>
{Object.values(eventTypes).map(eventType => {
return (
<button
className={`${styles.filterButton} ${
usedTypes[eventType] ? '' : styles.grey
} ${
eventFilter['message.type'] === eventType ? styles.active : ''
}`}
onClick={() => {
addFilter('message.type', eventType);
}}
>
{`${eventType.replace('inulax ', '')}(${
usedTypes[eventType] || 0
})`}
</button>
);
})}
</div>
<Table
data={processedData}
dataKey={'id'}
displayKeys={[
['type', 'Event type:'],
['storeClick', 'Store:'],
['time', 'Time:'],
['state', 'State:'],
['additionalData', 'Additional data:'],
]}
displayDataProcessor={data => {
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 : ''}
/>
</div>
);
}

View File

@ -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 (
<div
style={{
position: 'fixed',
width: '100vw',
height: '100vh',
top: 0,
left: 0,
backgroundColor: 'rgba(0, 0, 0 , 0.3)',
}}
>
<div
style={{
top: 'calc(50vh - 50px)',
left: 'calc(50vw - 125px)',
width: '250px',
backgroundColor: 'white',
border: '1px solid black',
position: 'fixed',
textAlign: 'center',
}}
>
<p>{children}</p>
<p>
<input
ref={inputRef}
type={'text'}
onKeyPress={({key}) => {
if (key === 'Enter') {
tryGatherData();
}
}}
/>
</p>
{error ? <p>Variable parsing error</p> : null}
<p>
<button
onClick={() => {
tryGatherData();
}}
>
OK
</button>
<button
onClick={() => {
closeModal();
}}
>
Cancel
</button>
</p>
</div>
</div>
);
}

View File

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

View File

@ -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: () => (
<Stores
nextStoreId={nextStoreId}
showFilteredEvents={showFilterEvents}
/>
),
},
{
id: 'events',
title: 'Events',
getComponents: () => (
<EventLog
setNextStore={id => {
setNextStoreId(id);
setActive('stores');
}}
setEventFilter={setEventFilter}
eventFilter={eventFilter}
/>
),
},
];
return (
<div>
<div style={{ marginBottom: '10px' }}>
{tabs.map(tab =>
tab.id === active ? (
<button
className={`${styles.tab} ${styles.active}`}
disabled={true}
>
{tab.title}
</button>
) : (
<button
className={styles.tab}
onClick={() => {
setActive(tab.id);
}}
>
{tab.title}
</button>
)
)}
</div>
{tabs.find(item => item.id === active).getComponent()}
</div>
);
}

View File

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

View File

@ -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 (
<div className={styles.wrapper}>
<div className={`${styles.table} ${styles.half}`}>
<div className={styles.row}>
<div className={`${styles.cell} ${styles.header}`}>{title}</div>
</div>
<div className={styles.scrollable}>
<span></span>
{data.map(row => (
<div
className={`${styles.row} ${
keyToDisplay === row[dataKey] ? styles.active : ''
}`}
onClick={() => {
setManualOverride(true);
setKeyToDisplay(
keyToDisplay === row[dataKey] ? null : row[dataKey]
);
}}
>
<div className={styles.cell}>{row?.[attr] || ''}</div>
</div>
))}
</div>
</div>
<div className={`${styles.table} ${styles.half} ${styles.displayData}`}>
<div className={styles.row}>
<div className={styles.cell}>
<b>Data:</b>
<button
className={styles.floatingButton}
onClick={() => {
setKeyToDisplay(null);
}}
>
X
</button>
</div>
</div>
<div className={styles.scrollable}>
<span></span>
<div className={styles.row}>
<div className={styles.cell}>
<Tree
data={
displayDataProcessor
? displayDataProcessor(displayRow)
: displayRow
}
indent={displayRow[displayKeys[0][0]]}
expand={true}
search={search}
forcedExpand={true}
/>
</div>
</div>
</div>
</div>
</div>
);
} else {
return (
<div className={styles.wrapper}>
<div className={styles.table}>
<div className={`${styles.row} ${styles.header}`}>
{displayKeys.map(([key, title]) => (
<div className={styles.cell}>{title}</div>
))}
</div>
{data.map(item => (
<div
onClick={() => {
setManualOverride(true);
setKeyToDisplay(item[dataKey]);
}}
className={styles.row}
>
{displayKeys.map(([key, title]) => (
<div className={styles.cell}>{item[key]}</div>
))}
</div>
))}
</div>
</div>
);
}
}

View File

@ -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 ? (
<div
style={{ fontFamily: 'monoSpace' }}
className={`${expanded ? 'expanded' : 'not-expanded'} ${className}`}
onClick={e => {
e.stopPropagation();
}}
>
<span
style={{ cursor: 'pointer' }}
onClick={() => {
setExpanded(!expanded);
}}
>
{new Array(Math.max(indent, 0)).fill(<span>&nbsp;</span>)}
{forcedExpand || isVNode ? null : expanded ? (
<span></span>
) : (
<span></span>
)}
{index === 0 || index ? (
<>
<b className={styles.purple}>{displayValue(index, search)}: </b>
</>
) : (
''
)}
{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})`
: '{ ... }'}
</span>
{expanded || isVNode ? (
isArray ? (
<>
{data.map((value, index) => {
return (
<div>
<Tree
data={value}
indent={indent + 4}
index={index}
search={search}
className={className}
onEdit={
onEdit
? (path, val) => {
onEdit(path.concat([index]), val);
}
: null
}
/>
</div>
);
})}
</>
) : isVNode ? (
data
) : isMap ? (
<div>
{data.entries.map(([key, value]) => {
return (
<Tree
data={{key, value}}
indent={indent + 4}
search={search}
className={className}
// TODO: editable sets
/>
);
})}
</div>
) : isSet ? (
data.values.map(item => {
return (
<div>
<Tree
data={item}
indent={indent + 4}
search={search}
className={className}
// TODO: editable sets
/>
</div>
);
})
) : (
Object.entries(data).map(([key, value]) => {
return (
<div>
<Tree
data={value}
indent={indent + 4}
index={key}
search={search}
className={className}
onEdit={
onEdit
? (path, val) => {
onEdit(path.concat([key]), val);
}
: null
}
/>
</div>
);
})
)
) : (
''
)}
</div>
) : (
<div className={'not-expanded'}>
{new Array(indent).fill(<span>&nbsp;</span>)}
<span className={`${className}`}>
{typeof index !== 'undefined' ? (
<>
<b className={styles.purple}>{displayValue(index, search)}: </b>
</>
) : (
''
)}
{displayValue(data, search)}
{onEdit && !isWeakSet && !isWeakMap ? ( // TODO: editable weak set and map
<>
<b
style={{ cursor: 'pointer' }}
onClick={() => {
setModal(true);
}}
>
</b>
{onEdit && modal ? (
<Modal
closeModal={() => {
setModal(false);
}}
then={data => {
onEdit([], data);
setModal(false);
}}
>
<h3>Edit value:</h3> {index}
</Modal>
) : null}
</>
) : null}
</span>
</div>
);
}

View File

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

View File

@ -0,0 +1,49 @@
<!--
~ Copyright (c) 2023 Huawei Technologies Co.,Ltd.
~
~ openInula is licensed under Mulan PSL v2.
~ You can use this software according to the terms and conditions of the Mulan PSL v2.
~ You may obtain a copy of Mulan PSL v2 at:
~
~ http://license.coscl.org.cn/MulanPSL2
~
~ THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
~ EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
~ MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
~ See the Mulan PSL v2 for more details.
-->
<!doctype html>
<html style="display: flex">
<head>
<meta charset="utf8">
<meta http-equiv="Content-Security-Policy"
content="default-src *; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' ">
<style>
html {
width: 100%;
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
#root {
width: 100%;
height: 100%;
}
</style>
<script src="inula.development.js"></script>
</head>
<body>
<div id="root"></div>
<script src="panelX.js"></script>
</body>
</html>

View File

@ -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(<span className={styles.highlighted}>{search}</span>);
} else {
result.push(parts[i / 2]);
}
}
return result;
}
export function displayValue(val: any, search = '') {
if (typeof val === 'boolean') {
return (
<span>
{highlight(val ? 'true' : 'false', search)}
</span>
);
}
if (val === '') {
return <span className={styles.red}>{'""'}</span>;
}
if (typeof val === 'undefined') {
return <span className={styles.grey}>{highlight('undefined', search)}</span>;
}
if (val === 'null') {
return <span className={styles.grey}>{highlight('null', search)}</span>;
}
if (typeof val === 'string') {
if (val.match(/^function\s?\(/)) {
return (
<span>
<i>ƒ</i>
{highlight(
val.match(/^function\s?\([\w,]*\)/g)[0].replace(/^function\s?/, ''),
search
)}
</span>
);
}
return <span className={styles.red}>"{highlight(val, search)}"</span>;
}
if (typeof val === 'number') {
return <span className={styles.blue}>{highlight('' + val, search)}</span>;
}
if (typeof val === 'function') {
const args = val.toString().match(/^function\s?\([\w,]*\)/g)[0].replace(/^function\s?/, '');
return (
<span>
<i>ƒ</i>
{highlight(args, search)}
</span>
);
}
if (typeof val === 'object') {
if (val?._type === 'WeakSet') {
return <span>WeakSet()</span>;
}
if (val?._type === 'WeakMap') {
return <span>WeakMap()</span>;
}
}
}
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 (
<span>
<i>ƒ</i>
{data.match(/^function\([\w,]*\)/g)[0].substring(8)}
</span>
);
}
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 (
<span>
<span className={styles.purple}>{key}</span>
<span>
<i>ƒ</i>
{value.match(/^function\([\w,]*\)/g)[0].substring(8)}
</span>
</span>
);
}
if (!value) {
return (
<span>
<span className={styles.purple}>{key}</span>:{displayValue(value)}
</span>
);
}
if (Array.isArray(value)) {
return (
<span>
<span className={styles.purple}>{key}</span>:{' '}
{`Array(${value.length})`}
</span>
);
}
if (typeof value === 'object') {
if ((value as any)?._type === 'WeakSet') {
return (
<span>
<span className={styles.purple}>{key}</span>: {'WeakSet()'}
</span>
);
}
if ((value as any)?._type === 'WeakMap') {
return (
<span>
<span className={styles.purple}>{key}</span>: {'WeakMap'}
</span>
);
}
if ((value as any)?._type === 'Set') {
return (
<span>
<span className={styles.purple}>{key}</span>:{' '}
{`Set(${(value as Set<any>).size})`}
</span>
);
}
if ((value as any)?._type === 'Map') {
return (
<span>
<span className={styles.purple}>{key}</span>:{' '}
{`Map(${(value as Map<any, any>).size})`}
</span>
);
}
// object
return (
<span>
<span className={styles.purple}>{key}</span>: {'{...}'}
</span>
);
}
return (
<span>
<span className={styles.purple}>{key}</span>: {displayValue(value)}
</span>
);
})}}`;
}
return data;
}
export function sendMessage(payload) {
chrome.runtime.sendMessage({
type: 'INULA_DEV_TOOLS',
payload,
from: DevToolPanel,
});
}

View File

@ -0,0 +1,416 @@
/*
* 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';
// 展示值为 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<string, any>): Array<string | number | symbol> => {
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 = <T>(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<any, any>[] | null,
depContexts: Array<ContextType<any>> | 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,
};
}

View File

@ -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<string>;
};
// 建立双向映射关系,当用户在修改属性值后,可以找到对应的 VNode
export let VNodeToIdMap: Map<VNode, number>;
export let IdToVNodeMap: Map<number, VNode>;
if (!VNodeToIdMap) {
VNodeToIdMap = new Map<VNode, number>();
}
if (!IdToVNodeMap) {
IdToVNodeMap = new Map<number, VNode>();
}
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<string> = [
'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;

View File

@ -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 (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
<path d={d} fill='currentColor' />
</svg>
);
}

View File

@ -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 (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
<path d='M4 3 L3 4 L7 8 L3 12 L4 13 L8 9 L12 13 L13 12 L9 8 L13 4 L12 3 L8 7z' fill='currentColor' />
</svg>
);
}

View File

@ -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 (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='1.3rem' height='1.3rem' margin-top='0.4rem'>
<path d='M0 0h24v24H0z' fill='none'/>
<path d='M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4 41 15.59 3l-2.17 2.17c12 96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41
3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34 04.67 09 1H4v2h2.81c1.04
1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6
8h-4v-2h4v2zm0-4h-4v-2h4v2z'
fill='currentColor' />
</svg>
);
}

View File

@ -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 (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='1.3rem' height='1.3rem'>
<path d='M0 0h24v24H0z' fill='none' />
<path d='M15.5 14h-.79l-.28-.27C15 41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91
16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99
5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'
fill='#343231' p-id='4151' />
</svg>
);
}

View File

@ -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 (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='1.3rem' height='1.3rem'>
<path d='M0 0h24v24H0z' fill='none'/>
<path d='M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12
17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3
3-1.34 3-3-1.34-3-3-3z'
fill='currentColor' />
</svg>
);
}

View File

@ -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 (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='1.3rem' height='1.3rem'>
<path d='M0 0h24v24H0z' fill='none' />
<path d='M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z'
fill='currentColor' />
</svg>
);
}

View File

@ -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 (
<svg t="1669105013009" className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2821" width="1rem" height="1rem">
<path d="M177.23392 943.96416c25.93792 0 50.3296-10.13248 68.70528-28.50816l208.18432-208.18944 29.36832-29.36832 38.90688 14.51008a323.70688 323.70688 0 0 0 112.85504 20.18304c86.48704 0 167.90528-33.664 229.05344-94.90432 83.4816-83.39456 113.4592-203.37152 83.99872-312.96512L795.60704 457.4208l-45.77792 45.77792-45.85984-45.77792-137.41568-137.41056-45.77792-45.87008 45.77792-45.7728 152.61696-152.61696a327.44448 327.44448 0 0 0-83.90656-10.99264c-86.48704 0-167.81824 33.66912-228.9664 94.90944C316.96896 248.89856 287.68256 383.13472 331.5712 501.57056l14.42816 38.90688-29.37344 29.36832-208.09728 208.19456c-18.37568 18.37568-28.51328 42.76736-28.51328 68.70528 0 25.93792 10.13248 50.3296 28.51328 68.7104a96.41472 96.41472 0 0 0 68.70528 28.50816m0 64.76288a161.67936 161.67936 0 0 1-114.57024-47.41632c-63.21152-63.29856-63.21152-165.84704 0-229.05344L270.848 524.07296c-51.18976-138.01984-21.30432-299.31008 89.57952-410.27072C436.352 37.9648 535.808 0 635.25888 0c68.4544 0 136.81664 17.95072 197.53472 53.77024l-220.38016 220.37504 137.41568 137.41568 220.38528-220.38016c88.02816 149.0944 68.0192 344.30976-60.12416 472.3712-75.83744 75.83232-175.37536 113.79712-274.83136 113.79712a389.6064 389.6064 0 0 1-135.35232-24.2176l-208.18944 208.18432c-31.60576 31.60064-73.088 47.4112-114.4832 47.4112z" p-id="2822"/>
</svg>
);
}

View File

@ -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 (
<svg width='1rem' height='1rem' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'>
<g fill='none' fill-rule='evenodd'>
<g stroke='currentColor'>
<path
stroke-width='.291'
fill='currentColor'
fill-rule='nonzero'
stroke-linecap='round'
stroke-linejoin='round'
d='M6 6l3.014 9 2.508-3.533L15 8.791z'
></path>
<path stroke-width='2' d='M10.417 10.417l2.87 2.87L15 15'></path>
</g>
<path
d='M12.188 0A2.812 2.812 0 0 1 15 2.813V5h-1V2.857A1.857 1.857 0 0 0 12.143 1H2.857A1.857 1.857 0 0 0 1 2.857v9.286C1 13.169 1.831 14 2.857 14H5v1H2.812A2.812 2.812 0 0 1 0 12.187V2.813A2.812 2.812 0 0 1 2.813 0h9.374z'
fill='currentColor'
fill-rule='nonzero'
></path>
</g>
</svg>
);
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
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 (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='8px' height='8px'>
<path d={d} fill='currentColor' />
</svg>
);
}

View File

@ -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.
-->
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem">
<path d="M720 192h-544A80.096 80.096 0 0 0 96 272v608C96 924.128 131.904 960 176 960h544c44.128 0 80-35.872 80-80v-608C800 227.904 764.128 192 720 192z m16 688c0 8.8-7.2 16-16 16h-544a16 16 0 0 1-16-16v-608a16 16 0 0 1 16-16h544a16 16 0 0 1 16 16v608z" fill="#ffffff"></path>
<path d="M848 64h-544a32 32 0 0 0 0 64h544a16 16 0 0 1 16 16v608a32 32 0 1 0 64 0v-608C928 99.904 892.128 64 848 64z" fill="#ffffff"></path>
<path d="M608 360H288a32 32 0 0 0 0 64h320a32 32 0 1 0 0-64zM608 520H288a32 32 0 1 0 0 64h320a32 32 0 1 0 0-64zM480 678.656H288a32 32 0 1 0 0 64h192a32 32 0 1 0 0-64z" fill="#ffffff"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -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.
-->
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem">
<path d="M224 217h-64v588h64V217z m640 0v588h-64V217h64z" fill="#ffffff"></path>
<path d="M732.649 278.902C679.222 303.256 600.918 320 511 320s-168.222-16.744-221.649-41.098C230.212 251.944 224 227.75 224 224s6.212-27.944 65.351-54.902C342.778 144.744 421.082 128 511 128s168.222 16.744 221.649 41.098C791.788 196.056 798 220.25 798 224s-6.212 27.944-65.351 54.902zM511 384c193.852 0 351-71.634 351-160S704.852 64 511 64s-351 71.634-351 160 157.148 160 351 160zM162 416c0 88.366 157.148 160 351 160s351-71.634 351-160h-64c0 3.75-6.212 27.944-65.351 54.902C681.222 495.256 602.918 512 513 512s-168.222-16.744-221.649-41.098C232.212 443.944 226 419.75 226 416h-64zM162 608c0 88.366 157.148 160 351 160s351-71.634 351-160h-64c0 3.75-6.212 27.944-65.351 54.902C681.222 687.256 602.918 704 513 704s-168.222-16.744-221.649-41.098C232.212 635.944 226 611.75 226 608h-64zM161 803c0 88.366 157.148 160 351 160s351-71.634 351-160h-64c0 3.75-6.212 27.944-65.351 54.902C680.222 882.256 601.918 899 512 899s-168.222-16.744-221.649-41.098C231.212 830.944 225 806.75 225 803h-64z" fill="#ffffff"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -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 (
<div
style={{
border: '1px solid black',
borderRadius: '2px',
width: '0.75rem',
height: '0.75rem',
padding: '1px',
backgroundColor: 'white',
display: 'inline-block',
position: 'relative',
cursor: 'pointer',
verticalAlign: 'sub',
marginBottom: '0.1rem'
}}
>
<div
style={{
width: '100%',
height: '100%',
backgroundColor: value? 'black' : 'white',
position: 'relative'
}}
></div>
</div>
);
}

View File

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

View File

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

View File

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

View File

@ -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);
}
export function injectCode(code) {
const script = document.createElement('script');
script.textContent = code;
ifNullThrows(document.documentElement).appendChild(script);
ifNullThrows(script.parentNode).removeChild(script);
}

View File

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

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