Compare commits

..

3 Commits

Author SHA1 Message Date
涂旭辉 06458da6db
!33 [inula]<feat> 新增 reconciler 模块
Merge pull request !33 from 涂旭辉/reconciler
2023-10-09 09:04:31 +00:00
13659257719 61ad99bdb1 Merge branch 'master' of https://gitee.com/openInula/inula into reconciler 2023-10-09 16:59:44 +08:00
13659257719 f8c5cb0bbe [inula]<feat> 新增 reconciler 模块 2023-10-09 16:53:51 +08:00
384 changed files with 2472 additions and 18850 deletions

View File

@ -1,3 +1,3 @@
**/node_modules
**/build/
build/
*.d.ts

View File

@ -22,7 +22,7 @@ module.exports = {
],
root: true,
plugins: ['jest', 'no-function-declare-after-return', 'react', '@typescript-eslint'],
plugins: ['jest', 'no-for-of-loops', 'no-function-declare-after-return', 'react', '@typescript-eslint'],
parser: '@typescript-eslint/parser',
parserOptions: {
@ -56,8 +56,8 @@ module.exports = {
'comma-dangle': ['error', 'only-multiline'],
'no-constant-condition': 'off',
'no-for-of-loops/no-for-of-loops': 'error',
'no-function-declare-after-return/no-function-declare-after-return': 'error',
'@typescript-eslint/ban-ts-comment': 'warn'
},
globals: {
isDev: true,

View File

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

View File

@ -1,8 +1,17 @@
---
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

3
.gitignore vendored
View File

@ -5,6 +5,3 @@ package-lock.json
pnpm-lock.yaml
/packages/**/node_modules
/packages/inula-cli/lib
build
/packages/inula-router/connectRouter
/packages/inula-router/router

View File

@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run commitlint

View File

@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint-commit

View File

@ -1,3 +0,0 @@
**/build
*.md
*.html

View File

@ -16,17 +16,17 @@
'use strict';
module.exports = {
printWidth: 120, // 一行120字符数如果超过会进行换行
tabWidth: 2, // tab等2个空格
useTabs: false, // 用空格缩进行
semi: true, // 行尾使用分号
singleQuote: true, // 字符串使用单引号
quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号
jsxSingleQuote: false, // 在JSX中使用双引号
trailingComma: 'es5', // 使用尾逗号(对象、数组等)
bracketSpacing: true, // 对象的括号间增加空格
bracketSameLine: false, // 将多行JSX元素的>放在最后一行的末尾
arrowParens: 'avoid', // 在唯一的arrow函数参数周围省略括号
vueIndentScriptAndStyle: false, // 不缩进Vue文件中的<script>和<style>标记内的代码
endOfLine: 'auto', // 仅限换行(\n
printWidth: 120, // 一行120字符数如果超过会进行换行
tabWidth: 2, // tab等2个空格
useTabs: false, // 用空格缩进行
semi: true, // 行尾使用分号
singleQuote: true, // 字符串使用单引号
quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号
jsxSingleQuote: false, // 在JSX中使用双引号
trailingComma: 'es5', // 使用尾逗号(对象、数组等)
bracketSpacing: true, // 对象的括号间增加空格
bracketSameLine: false, // 将多行JSX元素的>放在最后一行的末尾
arrowParens: 'avoid', // 在唯一的arrow函数参数周围省略括号
vueIndentScriptAndStyle: false, // 不缩进Vue文件中的<script>和<style>标记内的代码
endOfLine: 'lf', // 仅限换行(\n
};

View File

@ -1,51 +0,0 @@
version: '1.0'
name: branch-pipeline
displayName: BranchPipeline
stages:
- stage:
name: compile
displayName: 编译
steps:
- step: build@nodejs
name: build_nodejs
displayName: Nodejs 构建
# 支持8.16.2、10.17.0、12.16.1、14.16.0、15.12.0五个版本
nodeVersion: 14.16.0
# 构建命令:安装依赖 -> 清除上次打包产物残留 -> 执行构建 【请根据项目实际产出进行填写】
commands:
- npm install && rm -rf ./dist && npm run build
# 非必填字段开启后表示将构建产物暂存但不会上传到制品库中7天后自动清除
artifacts:
# 构建产物名字作为产物的唯一标识可向下传递支持自定义默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址
- name: BUILD_ARTIFACT
# 构建产物获取路径,是指代码编译完毕之后构建物的所在路径
path:
- ./dist
- step: publish@general_artifacts
name: publish_general_artifacts
displayName: 上传制品
# 上游构建任务定义的产物名默认BUILD_ARTIFACT
dependArtifact: BUILD_ARTIFACT
# 上传到制品库时的制品命名默认output
artifactName: output
dependsOn: build_nodejs
- stage:
name: release
displayName: 发布
steps:
- step: publish@release_artifacts
name: publish_release_artifacts
displayName: '发布'
# 上游上传制品任务的产出
dependArtifact: output
# 发布制品版本号
version: '1.0.0.0'
# 是否开启版本号自增,默认开启
autoIncrement: true
triggers:
push:
branches:
exclude:
- master
include:
- .*

View File

@ -1,49 +0,0 @@
version: '1.0'
name: master-pipeline
displayName: MasterPipeline
stages:
- stage:
name: compile
displayName: 编译
steps:
- step: build@nodejs
name: build_nodejs
displayName: Nodejs 构建
# 支持8.16.2、10.17.0、12.16.1、14.16.0、15.12.0五个版本
nodeVersion: 14.16.0
# 构建命令:安装依赖 -> 清除上次打包产物残留 -> 执行构建 【请根据项目实际产出进行填写】
commands:
- npm install && rm -rf ./dist && npm run build
# 非必填字段开启后表示将构建产物暂存但不会上传到制品库中7天后自动清除
artifacts:
# 构建产物名字作为产物的唯一标识可向下传递支持自定义默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址
- name: BUILD_ARTIFACT
# 构建产物获取路径,是指代码编译完毕之后构建物的所在路径
path:
- ./dist
- step: publish@general_artifacts
name: publish_general_artifacts
displayName: 上传制品
# 上游构建任务定义的产物名默认BUILD_ARTIFACT
dependArtifact: BUILD_ARTIFACT
# 上传到制品库时的制品命名默认output
artifactName: output
dependsOn: build_nodejs
- stage:
name: release
displayName: 发布
steps:
- step: publish@release_artifacts
name: publish_release_artifacts
displayName: '发布'
# 上游上传制品任务的产出
dependArtifact: output
# 发布制品版本号
version: '1.0.0.0'
# 是否开启版本号自增,默认开启
autoIncrement: true
triggers:
push:
branches:
include:
- master

View File

@ -1,36 +0,0 @@
version: '1.0'
name: pr-pipeline
displayName: PRPipeline
stages:
- stage:
name: compile
displayName: 编译
steps:
- step: build@nodejs
name: build_nodejs
displayName: Nodejs 构建
# 支持8.16.2、10.17.0、12.16.1、14.16.0、15.12.0五个版本
nodeVersion: 14.16.0
# 构建命令:安装依赖 -> 清除上次打包产物残留 -> 执行构建 【请根据项目实际产出进行填写】
commands:
- npm install && rm -rf ./dist && npm run build
# 非必填字段开启后表示将构建产物暂存但不会上传到制品库中7天后自动清除
artifacts:
# 构建产物名字作为产物的唯一标识可向下传递支持自定义默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址
- name: BUILD_ARTIFACT
# 构建产物获取路径,是指代码编译完毕之后构建物的所在路径
path:
- ./dist
- step: publish@general_artifacts
name: publish_general_artifacts
displayName: 上传制品
# 上游构建任务定义的产物名默认BUILD_ARTIFACT
dependArtifact: BUILD_ARTIFACT
# 上传到制品库时的制品命名默认output
artifactName: output
dependsOn: build_nodejs
triggers:
pr:
branches:
include:
- master

View File

@ -1,3 +0,0 @@
# Inula Contributing Guide
查看[贡献指南](https://docs.openinula.net/docs/%E8%B4%A1%E7%8C%AE%E6%8C%87%E5%8D%97)获取完整指南。

View File

@ -2,86 +2,78 @@
## 项目介绍
单词 Inula发音为[ˈɪnjʊlə]意为一类旋覆花属菊科的植物。openInula 是一款用于构建用户界面的 JavaScript 库,提供响应式 API 帮助开发者简单高效构建 Web 页面,比传统虚拟 DOM 方式渲染效率提升30%以上!同时 openInula 提供与 React 保持一致的 API并且提供5大常用功能组件状态管理器、路由、国际化、请求组件、应用脚手架以便开发者高效、高质量的构筑基于 openInula 的前端产品。
单词 Inula发音为[ˈɪnjʊlə]意为一类旋覆花属菊科的植物。openInula 是一款用于构建用户界面的 JavaScript 库,提供响应式 API 帮助开发者简单高效构建 web 页面,比传统虚拟 DOM 方式渲染效率提升30%以上!同时 InulaJS 提供与 React 保持一致的 API并且提供5大常用功能丰富的核心组件:状态管理器、路由、国际化、请求组件、应用脚手架,以便开发者高效、高质量的构筑基于 InulaJS 的前端产品。
## 技术架构
![](https://openinula-website.obs.ap-southeast-1.myhuaweicloud.com/misc/structure.png)
![输入图片说明](https://gitee.com/openInula/inula-docs/raw/master/static/img/structure.png)
## 核心能力
### 核心能力
### 响应式API
**响应式API**
openInula 通过监听状态变量的变化,以细粒度的依赖追踪机制来实现响应式更新,避免了虚拟 DOM 的开销。通过最小化重新渲染的范围从而进行高效的UI渲染。无需用户过度关注性能优化。
* openInula 通过最小化重新渲染的范围从而进行高效的UI渲染。这种方式避免了虚拟DOM的开销使得 openInula 在性能方面表现出色。
* openInula 通过比较变化前后的 JavaScript 对象以细粒度的依赖追踪机制来实现响应式更新,无需用户过度关注性能优化。
* 简洁API
1. openInula 提供了两组简洁直观的API--响应式 API 和与 React 一致的传统API使得开发者可以轻松地构建复杂的交互式界面。
2. openInula 简洁的API极大降低了开发者的学习成本开发者使用响应式API可以快速构建高效的前端界面。
>(实验性功能,可在 `reactive` 分支查看代码或使用 npm 仓中 experimental 版本体验)
**兼容 ReactAPI**
### 兼容 React API
提供与 React 一致的 API完全支持 React 生态,可将 React 应用可零修改切换至 openInula。
* 与 React 保持一致 API 的特性、可以无缝支持 React 生态。
* 使用传统 API 可以无缝将 React 项目切换至 openInulaReact 应用可零修改切换至 openInula。
### openInula 配套组件
#### 状态管理器 inula-X
**状态管理器/inula-X**
inula-X 是 openInula 默认提供的状态管理器。无需额外引入三方库,就可以简单实现跨组件/页面共享状态。
inula-X 是 openInula 默认提供的状态管理器,无需额外引入三方库,就可以简单实现跨组件/页面共享状态。
inula-X 与 Redux 比可创建多个 Store不需要在 Reducer 中返回 state 并且简化了 Action 和 Reducer 的创建步骤原生支持异步能力组件能做到精准重渲染。inula-X 均可使用函数组件、class 组件,能提供 redux 的适配接口及支持响应式的特点。
inula-X 与 Redux 相比,可创建多个 Store不需要在 Reducer 中返回 state 并且简化了 Action 和 Reducer 的创建步骤原生支持异步能力组件能做到精准重渲染。inula-X 均可使用函数组件、class 组件,能提供 redux 的适配接口及支持响应式的特点。
**路由/inula-router**
#### 路由 inula-router
inula-router 是 openInula 生态组建的一部分,为 openInula 提供前端路由的能力,是构建大型应用必要组件。
inula-router 涵盖 react-router、history、connect-react-router 的功能。
inula-router 为 openInula 提供前端路由的能力,是构建大型应用必要组件,涵盖 react-router、history、connect-react-router 的功能。
**请求/inula-request**
#### 请求 inula-request
inula-request 是 openInula 生态组件,涵盖常见的网络请求方式,并提供动态轮询钩子函数给用户更便捷的定制化请求体验。
inula-request 是 openInula 的网络请求组件,不仅涵盖常见的网络请求方式,还提供动态轮询钩子函数给用户更便捷的定制化请求体验。
**国际化/inula-intl**
#### 国际化 inula-intl
inula-intl 是基于 openInula 生态组件,其主要提供了国际化功能,涵盖了基本的国际化组件和钩子函数,便于用户在构建国际化能力时方便操作。
inula-intl 是基于 openInula 的国际化组件,涵盖了基本的国际化组件和钩子函数,允许用户更方便地构建国际化能力。
#### 调试工具 inula-dev-tools
**调试工具/inula-dev-tools**
inula-dev-tools 是一个为 openInula 开发者提供的强大工具集,能够方便地查看和编辑组件树、管理应用状态以及进行性能分析,极大提高了开发效率和诊断问题的便捷性。
#### 脚手架 create-inula
**脚手架/inula-cli**
create-inula 是一套用于创建 openInula 项目的脚手架工具。它预置了一系列项目模板,允许开发者通过命令行按需快速生成可运行的项目代码
inula-cli 是一套针对 openInula 的编译期插件它支持代码优化、JSX 语法转换以及代码分割,有助于提高应用的性能、可读性和可维护性
## 参与贡献
## openInula 文档
我们鼓励开发者以各种方式参与代码贡献、生态拓展或文档反馈,献您的原创内容,详细请参考[贡献指南](https://docs.openinula.net/docs/%E8%B4%A1%E7%8C%AE%E6%8C%87%E5%8D%97)
欢迎访问 openInula 官网文档仓库,参与 openInula 开发者文档开源项目,与我们一起完善开发者文档
### 官方链接
[访问官网](https://www.openinula.net/)
欢迎访问 openInula 官网与文档仓库,参与 openInula 开发者文档开源项目,与我们一起完善开发者文档。
## 代码仓地址
* 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)**
**参与贡献**
欢迎您参与[贡献](https://gitee.com/openinula/docs/blob/master/%E8%B4%A1%E7%8C%AE%E6%8C%87%E5%8D%97.md),我们鼓励开发者以各种方式参与文档反馈和贡献。
基于 umijs 与 openInula 的开发框架集成官方组件与UI、AIGC等功能开箱即用。
**[`VoerkaI18n`](https://github.com/zhangfisher/voerka-i18n/)**
适用于多框架的 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)
您可以对现有文档进行评价、简单更改、反馈文档质量问题、贡献您的原创内容,详细请参考[贡献文档](https://gitee.com/openinula/docs/blob/master/%E8%B4%A1%E7%8C%AE%E6%8C%87%E5%8D%97.md)。
## 许可协议
openInula 主要遵循 [Mulan Permissive Software License v2](http://license.coscl.org.cn/MulanPSL2) 协议,详情请参考各代码仓 LICENSE 声明。
openInula 主要遵循 Mulan Permissive Software License v2 协议,详情请参考各代码仓 LICENSE 声明。
## 联系方式
* 官方邮箱: [team@inulajs.org](mailto:team@inulajs.org)
team@inulajs.org
* 微信公众号:
![](https://www.openinula.net/assets/qrcode.inula-02f99d58.jpg)

View File

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

View File

@ -1,19 +0,0 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
module.exports = {
extends: ['@commitlint/config-conventional'],
'type-enum': ['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test', 'types'],
};

View File

@ -1,90 +1,69 @@
{
"name": "inula",
"name": "openinula",
"description": "OpenInula is a JavaScript framework library.",
"version": "0.0.1",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts --fix",
"lint-commit": "lint-staged",
"prettier": "prettier .prettierrc.js -w packages/**/*.{ts,tsx,js,jsx}",
"prettier": "prettier -w libs/**/*.ts",
"build:inula": "pnpm -F openinula build",
"test:inula": "pnpm -F openinula test",
"test:inula-intl": "pnpm -F inula-intl test",
"test:inula-request": "pnpm -F inula-request test",
"test:inula-router": "pnpm -F inula-router test",
"build:inula-cli": "pnpm -F inula-cli build",
"build:inula-intl": "pnpm -F inula-intl build",
"build:inula-request": "pnpm -F inula-request build",
"build:inula-router": "pnpm -F inula-router build",
"commitlint": "commitlint --config commitlint.config.js -e",
"postinstall": "husky install"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier .prettierrc.js -w"
]
"build:inula-router": "pnpm -F inula-router build"
},
"devDependencies": {
"@babel/core": "7.23.7",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
"@babel/plugin-proposal-optional-chaining": "7.21.0",
"@babel/plugin-proposal-private-methods": "7.18.6",
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
"@babel/plugin-syntax-jsx": "7.23.3",
"@babel/plugin-transform-arrow-functions": "7.23.3",
"@babel/plugin-transform-block-scoped-functions": "7.23.3",
"@babel/plugin-transform-block-scoping": "7.23.4",
"@babel/plugin-transform-classes": "7.23.8",
"@babel/plugin-transform-computed-properties": "7.23.3",
"@babel/plugin-transform-destructuring": "7.23.3",
"@babel/plugin-transform-for-of": "7.23.6",
"@babel/plugin-transform-literals": "7.23.3",
"@babel/plugin-transform-object-assign": "7.23.3",
"@babel/plugin-transform-object-super": "7.23.3",
"@babel/plugin-transform-parameters": "7.23.3",
"@babel/plugin-transform-react-jsx": "7.23.4",
"@babel/plugin-transform-react-jsx-source": "^7.23.3",
"@babel/plugin-transform-runtime": "7.23.7",
"@babel/plugin-transform-shorthand-properties": "7.23.3",
"@babel/plugin-transform-spread": "7.23.3",
"@babel/plugin-transform-template-literals": "7.23.3",
"@babel/preset-env": "7.23.8",
"@babel/preset-typescript": "7.23.3",
"@babel/runtime": "7.23.8",
"@commitlint/cli": "^17.8.1",
"@commitlint/config-conventional": "^17.8.1",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-node-resolve": "^15.2.3",
"@babel/core": "7.16.7",
"@babel/plugin-proposal-class-properties": "7.16.7",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
"@babel/plugin-proposal-object-rest-spread": "7.16.7",
"@babel/plugin-proposal-optional-chaining": "7.16.7",
"@babel/plugin-proposal-private-methods": "7.16.7",
"@babel/plugin-proposal-private-property-in-object": "7.16.7",
"@babel/plugin-syntax-jsx": "7.16.7",
"@babel/plugin-transform-arrow-functions": "7.16.7",
"@babel/plugin-transform-block-scoped-functions": "7.16.7",
"@babel/plugin-transform-block-scoping": "7.16.7",
"@babel/plugin-transform-classes": "7.16.7",
"@babel/plugin-transform-computed-properties": "7.16.7",
"@babel/plugin-transform-destructuring": "7.16.7",
"@babel/plugin-transform-for-of": "7.16.7",
"@babel/plugin-transform-literals": "7.16.7",
"@babel/plugin-transform-object-assign": "7.16.7",
"@babel/plugin-transform-object-super": "7.16.7",
"@babel/plugin-transform-parameters": "7.16.7",
"@babel/plugin-transform-react-jsx": "7.16.7",
"@babel/plugin-transform-react-jsx-source": "^7.16.7",
"@babel/plugin-transform-runtime": "7.16.7",
"@babel/plugin-transform-shorthand-properties": "7.16.7",
"@babel/plugin-transform-spread": "7.16.7",
"@babel/plugin-transform-template-literals": "7.16.7",
"@babel/preset-env": "7.16.7",
"@babel/preset-typescript": "7.16.7",
"@babel/runtime": "7.16.7",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^4.0.0",
"@types/jest": "^29.5.11",
"@types/jest": "^26.0.24",
"@types/node": "^17.0.18",
"@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "6.18.1",
"@babel/parser": "^7.24.7",
"magic-string": "^0.30.10",
"babel-jest": "^29.7.0",
"@typescript-eslint/eslint-plugin": "4.8.0",
"@typescript-eslint/parser": "4.8.0",
"babel-jest": "^27.5.1",
"ejs": "^3.1.8",
"eslint": "^8.56.0",
"eslint": "7.13.0",
"eslint-config-prettier": "^6.9.0",
"eslint-plugin-jest": "^22.15.0",
"eslint-plugin-no-for-of-loops": "^1.0.0",
"eslint-plugin-no-function-declare-after-return": "^1.0.0",
"eslint-plugin-react": "7.14.3",
"husky": "^8.0.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^15.2.0",
"openinula": "workspace:*",
"prettier": "^3.1.1",
"rollup": "^2.79.1",
"rollup-plugin-dts": "^6.1.0",
"jest": "^25.5.4",
"jest-environment-jsdom-sixteen": "^1.0.3",
"prettier": "2.6.2",
"rollup": "^2.75.5",
"rollup-plugin-execute": "^1.1.1",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-polyfill-node": "^0.13.0",
"ts-jest": "^29.1.1",
"typescript": "^4.9.5"
"typescript": "4.2.3"
},
"engines": {
"node": ">=10.x",

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
'use strict';
module.exports = {
printWidth: 120, // 一行120字符数如果超过会进行换行
tabWidth: 2, // tab等2个空格
useTabs: false, // 用空格缩进行
semi: true, // 行尾使用分号
singleQuote: true, // 字符串使用单引号
quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号
jsxSingleQuote: false, // 在JSX中使用双引号
trailingComma: 'es5', // 使用尾逗号(对象、数组等)
bracketSpacing: true, // 对象的括号间增加空格
bracketSameLine: false, // 将多行JSX元素的>放在最后一行的末尾
arrowParens: 'avoid', // 在唯一的arrow函数参数周围省略括号
vueIndentScriptAndStyle: false, // 不缩进Vue文件中的<script>和<style>标记内的代码
endOfLine: 'lf', // 仅限换行(\n
};

View File

@ -128,9 +128,9 @@ class BasicGenerator extends Generator {
if (fs.existsSync(fullpath)) {
this.traverseDirCapture(fullpath, dirCallback, fileCallback);
}
} else {
fileCallback(fullpath);
continue;
}
fileCallback(fullpath);
}
}
@ -140,9 +140,9 @@ class BasicGenerator extends Generator {
if (fs.lstatSync(fullpath).isDirectory()) {
this.traverseDirBubble(fullpath, dirCallback, fileCallback);
dirCallback(fullpath);
} else {
fileCallback(fullpath);
continue;
}
fileCallback(fullpath);
}
}

View File

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

View File

@ -34,9 +34,7 @@ function App() {
<h2>了解更多</h2>
<p>
要了解 Inula查看{' '}
<a href="https://openinula.net/" target="_blank">
Inula 官网
</a>
<a href="https://openinula.com/" target="_blank">Inula 官网</a>
</p>
</div>
</div>

View File

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

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 './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 class="content">
<div class="card animate__animated animate__zoomIn">
<h2>开始吧</h2>
<p>
编辑 <code>src/App.js</code>
</p>
</div>
<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

@ -13,7 +13,7 @@
* See the Mulan PSL v2 for more details.
*/
import Panel from './Panel';
import Inula from 'openinula';
import App from './App';
// 这里导出 Panel 为了加载 Panel.less
export default Panel;
Inula.render(<App />, document.getElementById('root'));

View File

@ -17,7 +17,7 @@ const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.jsx',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
@ -78,7 +78,4 @@ module.exports = {
port: 9000,
open: true,
},
resolve: {
extensions: ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'],
},
};

View File

@ -32,8 +32,8 @@ const generatorType = fs
});
const runGenerator = async (templatePath, { name = '', cwd = process.cwd(), args = {} }) => {
let currentPath;
return new Promise(resolve => {
let currentPath;
if (name) {
mkdirp.sync(name);
currentPath = path.join(cwd, name);
@ -62,17 +62,7 @@ const run = async config => {
}
process.emit('message', { type: 'prompt' });
let { type, name } = config;
if (!name) {
const answers = await inquirer.prompt([
{
name: 'projectName',
message: 'Project name',
type: 'input',
},
]);
config.name = answers.projectName;
}
let { type } = config;
if (!type) {
const answers = await inquirer.prompt([
{

View File

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

View File

@ -0,0 +1,55 @@
/*
* 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 = {
'parser': 'babel-eslint',
'env': {
'amd': true,
'es6': true,
'browser': true,
'node': false
},
'parserOptions': {
'ecmaVersion': 6,
'sourceType': 'module',
'ecmaFeatures': {
'jsx': true
}
},
'ignorePatterns': [
"src/template"
],
'rules': {
'indent': [
'error',
4,
{
SwitchCase: 1,
flatTernaryExpressions: true
}
],
'no-unused-vars': 'off', // 允许变量声明后未使用
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
"no-underscore-dangle": ["off", "always"], // 允许私有变量 _xxx的变量命名方式
'filenames/match-exported': 0,
'consistent-return': 0,
"comma-dangle": [2, "never"], // 组和对象键值对最后一个逗号, never参数不能带末尾的逗号, always参数必须带末尾的逗号
'global-require': 0, // 允许require语句不出现在顶层中
'no-nested-ternary': 0, // 允许嵌套三元表达式
'no-unused-expressions': 0, // 允许使用未执行的表达式。比如fn是一个函数允许 fn && fn()
'no-throw-literal': 0, // 允许throw抛出对象格式
'@typescript-eslint/member-ordering': 0 // 禁用TypeScript声明规范
}
}

View File

@ -0,0 +1,3 @@
node_modules/
webpack/
public/

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.
*/
export default {
printWidth: 120, // 一行120字符数如果超过会进行换行
tabWidth: 2, // tab等2个空格
useTabs: false, // 用空格缩进行
semi: true, // 行尾使用分号
singleQuote: true, // 字符串使用单引号
quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号
jsxSingleQuote: false, // 在JSX中使用双引号
trailingComma: 'es5', // 使用尾逗号(对象、数组等)
bracketSpacing: true, // 对象的括号间增加空格
jsxBracketSameLine: false, // 将多行JSX元素的>放在最后一行的末尾
arrowParens: 'avoid', // 在唯一的arrow函数参数周围省略括号
vueIndentScriptAndStyle: false, // 不缩进Vue文件中的<script>和<style>标记内的代码
endOfLine: 'lf', // 仅限换行(\n
};

View File

@ -2,23 +2,23 @@
## 一、安装使用
### 安装Node.js
### 安装Nodejs
inula-cli的运行需要依赖Node.js使用前请确保您的电脑已安装Node.js并且版本在16以上。您可以通过在控制台执行以下命令来确认您的版本。
inula-cli的运行需要依赖Nodejs使用前请确保您的电脑已安装Nodejs并且版本在16以上。您可以通过在控制台执行以下命令来确认您的版本。
```shell
```
>node -v
v16.4.0
```
如果您没有安装Node.js或者Node.js版本不满足条件推荐使用nvm工具安装和管理Node.js版本。
如果您没有安装Nodejs或者Nodejs版本不满足条件推荐使用nvm工具安装和管理Nodejs版本。
nvm最新版本下载: [https://github.com/coreybutler/nvm-windows/releases](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fcoreybutler%2Fnvm-windows%2Freleases)
安装nvm之后可以通过如下命令安装Node.js:
安装nvm之后可以通过如下命令安装Nodejs:
```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。Node.js安装会自带npm工具用于管理模块您可以直接运行如下命令
为了方便使用inula-cli的功能推荐您全局安装inula-cli。Nodejs安装会自带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,3 +710,6 @@ export default {
}
}
```

View File

@ -17,4 +17,4 @@
import run from '../lib/cli/cli.js';
run();
run();

View File

@ -1,16 +0,0 @@
/*
* 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 'crequire';

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import webpack from 'webpack';
import { build } from 'vite';
export default (api: any) => {
api.registerCommand({
name: 'build',
description: 'build application for production',
initialState: api.buildConfig,
fn: async function (args: any, state: any) {
switch (api.compileMode) {
case 'webpack':
if (state) {
api.applyHook({ name: 'beforeCompile', args: state });
state.forEach((s: any) => {
webpack(s.config, (err: any, stats: any) => {
if (err || stats.hasErrors()) {
api.logger.error(`Build failed.err: ${err}, stats:${stats}`);
}
});
});
} else {
api.logger.error(`Build failed. Can't find build config.`);
}
break;
case 'vite':
if (state) {
api.applyHook({ name: 'beforeCompile' });
build(state);
} else {
api.logger.error(`Build failed. Can't find build config.`);
}
break;
}
},
});
};

View File

@ -19,6 +19,7 @@ import { createServer } from 'vite';
import { API } from '../../../types/types';
import setupProxy from '../../../utils/setupProxy.js';
export default (api: API) => {
api.registerCommand({
name: 'dev',
@ -45,7 +46,7 @@ export default (api: API) => {
if (api.userConfig.devBuildConfig.devProxy) {
devServerOptions.onBeforeSetupMiddleware = (devServer: WebpackDevServer) => {
setupProxy(devServer.app, api);
};
}
}
api.applyHook({
@ -57,7 +58,7 @@ export default (api: API) => {
api.applyHook({ name: 'afterStartDevServer' });
});
} else {
api.logger.error("Can't find config");
api.logger.error('Can\'t find config');
}
break;
case 'vite':
@ -70,7 +71,7 @@ export default (api: API) => {
server.printUrls();
});
} else {
api.logger.error("Can't find config");
api.logger.error('Can\'t find config');
}
break;
default:

View File

@ -39,11 +39,9 @@ export default (api: API) => {
switch (args._[0]) {
case 'jest':
{
args._.shift();
const isESM = api.packageJson['type'] === 'module';
await generateJest(args, api.cwd, isESM);
}
args._.shift();
const isESM = api.packageJson['type'] === 'module';
await generateJest(args, api.cwd, isESM);
break;
default:
}
@ -52,7 +50,7 @@ export default (api: API) => {
};
const generateJest = async (args: yargsParser.Arguments, cwd: string, isESM: boolean) => {
let isTs = false;
let isTs: boolean = false;
if (args['ts']) {
isTs = true;
} else {

View File

@ -25,7 +25,7 @@ export default (api: any) => {
initialState: api.userConfig.remoteProxy,
fn: async function (args: any, state: any) {
if (!state) {
api.logger.error('Invalid proxy config!');
api.logger.error(`Invalid proxy config!`);
return;
}
const app = express();

View File

@ -46,7 +46,7 @@ export default async function run() {
initializeEnv();
if (command === 'version' || command === 'help') {
process.env.INNER_COMMAND = 'true';
process.env.INNER_COMMAND = "true"
}
switch (command) {
@ -61,9 +61,9 @@ export default async function run() {
break;
}
let enableDebug = false;
let enableDebug: boolean = false;
if (process.env.DEBUG === 'true') {
if (process.env.DEBUG === "true") {
enableDebug = true;
}

View File

@ -89,7 +89,7 @@ export default class Config {
getConfigFile(): string | null {
const configFileList: string[] = DEFAULT_CONFIG_FILES.map(f => join(this.cwd, f));
for (const configFile of configFileList) {
for (let configFile of configFileList) {
if (existsSync(configFile)) {
return configFile;
}

View File

@ -46,11 +46,11 @@ export default class Hub {
userConfig: UserConfig = {};
packageJson: PackageJSON;
stage: ServiceStage = ServiceStage.uninitialized;
buildConfig: { name: string; config: Record<string, unknown> }[] = [];
buildConfig: {name:string, config: object}[] = [];
pluginManager: Plugin;
buildConfigPath: BuildConfig[] = [];
devBuildConfig: Record<string, unknown> = {};
compileMode = '';
devBuildConfig: object = {};
compileMode: string = '';
builtInPlugins: string[] = [];
pluginPaths: string[] = [];
devProxy: DevProxy | null = null;
@ -95,7 +95,7 @@ export default class Hub {
this.userConfig = await this.configManager.getUserConfig();
// 设置编译模式
this.setCompileMode();
this.setCompileMode()
// 获取编译配置
await this.analyzeBuildConfig();
@ -135,8 +135,8 @@ export default class Hub {
: this.pluginManager.commands[command];
if (commands === undefined) {
this.logger.error(`Invalid command ${command}`);
return;
this.logger.error(`Invalid command ${command}`)
return
}
const { fn } = commands as ICommand;
@ -150,22 +150,21 @@ export default class Hub {
async analyzeBuildConfig() {
if (this.userConfig.devBuildConfig) {
let { path } = this.userConfig.devBuildConfig;
const { env } = this.userConfig.devBuildConfig;
let { name, path, env } = this.userConfig.devBuildConfig;
path = isAbsolute(path) ? path : join(process.cwd(), path);
if (!existsSync(path)) {
this.logger.warn(`Cant't find dev build config. Path is ${path}`);
return;
}
this.logger.debug(`Find dev build config. Path is ${path}`);
const bc = await loadModule<Record<string, unknown> | ((...args: any[]) => any)>(path);
let bc = await loadModule<object | Function>(path);
if (bc == undefined) {
return;
}
let finalBc = {};
if (typeof bc === 'function') {
finalBc = bc(env);
finalBc = bc(env)
this.devBuildConfig = finalBc;
return;
}
@ -176,54 +175,55 @@ export default class Hub {
}
}
if (!this.userConfig.buildConfig) {
switch (this.compileMode) {
case 'webpack':
this.buildConfigPath.push({ name: 'default', path: './webpack.config.js' });
this.buildConfigPath.push({name:'default', path:'./webpack.config.js'})
break;
case 'vite':
this.buildConfigPath.push({ name: 'default', path: './vite.config.js' });
this.buildConfigPath.push({name:'default', path:'./vite.config.js'})
break;
default:
this.logger.warn(`Unknown compile mode ${this.compileMode}`);
break;
}
} else {
this.userConfig.buildConfig.forEach(userBuildConfig => {
this.userConfig.buildConfig.forEach((userBuildConfig) => {
if (typeof userBuildConfig === 'object') {
this.buildConfigPath.push(userBuildConfig);
}
});
})
}
this.buildConfigPath.forEach(async config => {
let { path } = config;
const { name } = config;
this.buildConfigPath.forEach(async (config) => {
let {name, path} = config;
path = isAbsolute(path) ? path : join(process.cwd(), path);
if (!existsSync(path)) {
this.logger.debug(`Cant't find build config. Path is ${path}`);
return;
}
this.logger.debug(`Find build config. Path is ${path}`);
const bc = await loadModule<Record<string, unknown> | ((...args: any[]) => any)>(path);
let bc = await loadModule<object | Function >(path);
if (bc == undefined) {
return;
}
let finalBc = {};
if (typeof bc === 'function') {
finalBc = bc(config.env);
this.buildConfig.push({ name: name, config: finalBc });
finalBc = bc(config.env)
this.buildConfig.push({name: name, config: finalBc});
return;
}
this.buildConfig.push({ name: name, config: bc });
});
this.buildConfig.push({name: name, config: bc});
})
}
getConfigName(name: string): string {
name = name.replace('webpack.', '');
name = name.replace('.js', '');
name = name.replace('.ts', '');
return name;
return name
}
}

View File

@ -36,7 +36,7 @@ export interface IPlugin {
id: string;
key: string;
path: string;
apply: (...args: any[]) => any;
apply: Function;
}
export default class Plugin {
@ -57,7 +57,7 @@ export default class Plugin {
} = {};
hub: Hub;
logger: Logger;
registerFunction: ((...args: any[]) => any)[] = [];
registerFunction: Function[] = [];
// 解决调用this[props]时ts提示属性未知
[key: string]: any;
@ -110,7 +110,7 @@ export default class Plugin {
});
for (const obj of objs) {
const module: ((...args: any[]) => any) | undefined = await loadModule(obj.path);
const module: Function | undefined = await loadModule(obj.path);
if (module) {
try {
module(obj.api);
@ -135,11 +135,15 @@ export default class Plugin {
return new Proxy(pluginAPI, {
get: (target: PluginAPI, prop: string) => {
if (['userConfig', 'devBuildConfig', 'buildConfig', 'compileMode', 'packageJson', 'cwd'].includes(prop)) {
return typeof this.hub[prop] === 'function' ? this.hub[prop].bind(this.hub) : this.hub[prop];
return typeof this.hub[prop] === 'function'
? this.hub[prop].bind(this.hub)
: this.hub[prop];
}
if (['setStore', 'logger', 'commands'].includes(prop)) {
return typeof this[prop] === 'function' ? this[prop].bind(this) : this[prop];
return typeof this[prop] === 'function'
? this[prop].bind(this)
: this[prop];
}
return target[prop];

View File

@ -55,11 +55,11 @@ export default class PluginAPI {
this.register(hook);
}
registerMethod(fn: (...args: any[]) => any) {
registerMethod(fn: Function) {
this.manager.registerFunction.push(fn);
}
async applyHook(name: string, args?: any) {
async applyHook(name: string, args?: any ) {
const hooks: IHook[] = this.manager.hooks[name] || [];
let config: any = undefined;
for (const hook of hooks) {

View File

@ -19,8 +19,11 @@ import { Logger } from '../utils/logger.js';
import type * as http from 'http';
import type * as express from 'express';
type Request = express.Request;
type Response = express.Response;
interface Request extends express.Request {
}
interface Response extends express.Response {
}
export interface IDep {
[name: string]: string;
@ -37,7 +40,7 @@ export interface IPlugin {
id: string;
key: string;
path: string;
apply: (...args: any[]) => any;
apply: Function;
config?: IPluginConfig;
isPreset?: boolean;
@ -45,7 +48,7 @@ export interface IPlugin {
export interface IPluginConfig {
default?: any;
onChange?: string | ((...args: any[]) => any);
onChange?: string | Function;
}
export interface IHook {
@ -94,8 +97,8 @@ export interface API {
(hook: IHook): void;
};
registerMethod: {
(method: (...args: any[]) => any): void;
};
(method: Function): void;
}
applyHook: {
(opts: applyHookConfig): void;
};
@ -133,22 +136,22 @@ export interface MockConfig {
export interface DevBuildConfig {
name: string;
path: string;
args?: Record<string, unknown>;
env?: Record<string, unknown>;
args?: object;
env?: object;
devProxy?: DevProxy;
}
export interface DevProxy {
target: string;
matcher: (pathname: string, req: Request) => boolean;
matcher: ((pathname: string, req: Request) => boolean);
onProxyRes: (proxyRes: http.IncomingMessage, req: Request, res: Response) => void;
}
export interface BuildConfig {
name: string;
path: string;
args?: Record<string, unknown>;
env?: Record<string, unknown>;
args?: object;
env?: object;
}
export type ExportUserConfig = UserConfig | Promise<UserConfig>;
@ -162,3 +165,4 @@ export interface Arguments {
'--'?: Array<string | number>;
[argName: string]: any;
}

View File

@ -52,7 +52,7 @@ const buildConfig = async (fileName: string, format: 'esm' | 'cjs' = 'esm'): Pro
return {
loader: args.path.endsWith('.ts') ? 'ts' : 'js',
contents: contents,
contents: contents
};
});
},

View File

@ -27,6 +27,7 @@ export async function loadModule<T>(filePath: string): Promise<T | undefined> {
const isTsFile: boolean = filePath.endsWith('ts');
const isJsFile: boolean = filePath.endsWith('js');
let content: T | undefined;
// js文件可以直接通过import引用

View File

@ -15,8 +15,8 @@
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
INFO = 1,
WARN = 2,
ERROR = 3,
}

View File

@ -15,7 +15,7 @@
import chokidar from 'chokidar';
import bodyParser from 'body-parser';
import { globSync } from 'glob';
import {globSync} from 'glob';
import { join } from 'path';
import { createRequire } from 'module';
@ -38,15 +38,13 @@ function getMocksFile() {
const mockFiles = globSync('**/*.js', {
cwd: mockDir,
});
const ret = mockFiles.reduce((mocks: any, mockFile: string) => {
let ret = mockFiles.reduce((mocks: any, mockFile: string) => {
if (!mockFile.startsWith('_')) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const file = require(join(mockDir, mockFile));
mocks = {
...mocks,
...file,
...require(join(mockDir, mockFile)),
};
console.log('mockFile', file);
console.log('mockFile', require(join(mockDir, mockFile)));
}
return mocks;
@ -56,8 +54,8 @@ function getMocksFile() {
}
function generateRoutes(app: any) {
const mockStartIndex = app._router.stack.length;
let mocks: Mock = {};
let mockStartIndex = app._router.stack.length,
mocks: Mock = {};
try {
mocks = getMocksFile();
@ -95,8 +93,8 @@ function generateRoutes(app: any) {
respond instanceof Function
? respond
: (_req: any, res: { send: (arg0: any) => void }) => {
res.send(respond);
}
res.send(respond);
}
);
} catch (error) {
console.error(error);

View File

@ -17,14 +17,12 @@ import { createProxyMiddleware } from 'http-proxy-middleware';
import { API } from '../types/types';
export default (app: any, api: API) => {
const { devProxy } = api.userConfig.devBuildConfig;
app.use(
createProxyMiddleware(devProxy.matcher, {
target: devProxy.target,
secure: false,
changeOrigin: true,
ws: false,
onProxyRes: devProxy.onProxyRes,
})
);
};
const { devProxy } = api.userConfig.devBuildConfig;
app.use(createProxyMiddleware(devProxy.matcher, {
target: devProxy.target,
secure: false,
changeOrigin: true,
ws: false,
onProxyRes: devProxy.onProxyRes
}));
}

View File

@ -16,7 +16,8 @@
import { dirname } from 'path';
import { readFileSync, writeFileSync } from 'fs';
import resolve from 'resolve';
import crequire from 'crequire';
// @ts-ignore
import crequire from 'crequire'
import { createRequire } from 'module';
const require = createRequire(import.meta.url);

View File

@ -11,16 +11,9 @@
"moduleResolution": "node",
"esModuleInterop": true,
},
"include": [
"src/**/*",
"./externals.d.ts"
],
"exclude": [
"node_modules",
"**/*.spec.ts",
"./src/template/**/*"
],
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.spec.ts", "./src/template/**/*"],
"ts-node": {
"esm": true,
},
}
}

View File

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

View File

@ -1,21 +0,0 @@
/*
* 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

@ -1,54 +0,0 @@
{
"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

@ -1,80 +0,0 @@
/*
* 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

@ -1,283 +0,0 @@
/*
* 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

@ -1,279 +0,0 @@
/*
* 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

@ -1,440 +0,0 @@
/*
* 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

@ -1,18 +0,0 @@
/*
* 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

@ -1,44 +0,0 @@
/*
* 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

@ -1,48 +0,0 @@
/*
* 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

@ -1,79 +0,0 @@
/*
* 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

@ -1,26 +0,0 @@
/*
* 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

@ -1,135 +0,0 @@
/*
* 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

@ -1,17 +0,0 @@
/*
* 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

@ -1,78 +0,0 @@
/*
* 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

@ -1,318 +0,0 @@
/*
* 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

@ -1,32 +0,0 @@
/*
* 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

@ -1,95 +0,0 @@
/*
* 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

@ -1,68 +0,0 @@
/*
* 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

@ -1,283 +0,0 @@
/*
* 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

@ -1,171 +0,0 @@
/*
* 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

@ -1,460 +0,0 @@
/*
* 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

@ -1,96 +0,0 @@
/*
* 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

@ -1,88 +0,0 @@
/*
* 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

@ -1,30 +0,0 @@
<!--
~ 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

@ -1,35 +0,0 @@
{
"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

@ -1,117 +0,0 @@
/*
* 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

@ -1,417 +0,0 @@
/*
* 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

@ -1,35 +0,0 @@
<!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

@ -1,16 +0,0 @@
/*
* 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

@ -1,74 +0,0 @@
/*
* 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

@ -1,125 +0,0 @@
/*
* 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

@ -1,309 +0,0 @@
/*
* 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

@ -1,386 +0,0 @@
/*
* 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

@ -1,103 +0,0 @@
/*
* 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

@ -1,197 +0,0 @@
/*
* 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

@ -1,76 +0,0 @@
/*
* 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

@ -1,74 +0,0 @@
/*
* 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

@ -1,156 +0,0 @@
/*
* 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

@ -1,226 +0,0 @@
/*
* 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

@ -1,18 +0,0 @@
/*
* 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

@ -1,49 +0,0 @@
<!--
~ 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

@ -1,219 +0,0 @@
/*
* 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

@ -1,391 +0,0 @@
/*
* 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

@ -1,223 +0,0 @@
/*
* 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

@ -1,33 +0,0 @@
/*
* 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

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

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