Compare commits
3 Commits
master
...
reconciler
Author | SHA1 | Date |
---|---|---|
|
06458da6db | |
|
61ad99bdb1 | |
|
f8c5cb0bbe |
|
@ -1,3 +1,3 @@
|
|||
**/node_modules
|
||||
**/build/
|
||||
build/
|
||||
*.d.ts
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
---
|
||||
name: 模板名称,在新建pr时候能看到
|
||||
about: 模板描述,对应的pr模板卡片展示时候能看到,介绍模板
|
||||
---
|
||||
**PR 描述:** [请描述提交此 PR 的背景、目的、所做的更改以及如何测试此 PR]
|
||||
|
||||
**关联的 Issues:** [请列出与此 PR 相关的 issue 编号]
|
||||
|
||||
**检查项(无需修改,提交后界面上可勾选):**
|
||||
- [ ] 代码已经被审查
|
||||
**TODO(可选)**
|
||||
- [ ] 任务1
|
||||
- [ ] ...
|
||||
|
||||
**检查项:**
|
||||
|
||||
- [ ] 代码已经被检视
|
||||
- [ ] 代码符合项目的代码标准和最佳实践
|
||||
- [ ] 代码已经通过所有测试用例
|
||||
- [ ] 代码不影响现有功能的正常使用
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,7 +5,3 @@ package-lock.json
|
|||
pnpm-lock.yaml
|
||||
/packages/**/node_modules
|
||||
/packages/inula-cli/lib
|
||||
build
|
||||
/packages/inula-router/connectRouter
|
||||
/packages/inula-router/router
|
||||
.inula-max
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npm run commitlint
|
|
@ -1,4 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npm run lint-commit
|
|
@ -1,3 +0,0 @@
|
|||
**/build
|
||||
*.md
|
||||
*.html
|
|
@ -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)
|
||||
};
|
||||
|
|
|
@ -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:
|
||||
- .*
|
|
@ -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
|
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||
# Inula Contributing Guide
|
||||
|
||||
查看[贡献指南](https://docs.openinula.net/docs/%E8%B4%A1%E7%8C%AE%E6%8C%87%E5%8D%97)获取完整指南。
|
80
README.md
80
README.md
|
@ -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 的前端产品。
|
||||
|
||||
## 技术架构
|
||||
|
||||

|
||||

|
||||
|
||||
## 核心能力
|
||||
### 核心能力
|
||||
|
||||
### 响应式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 项目切换至 openInula,React 应用可零修改切换至 openInula。
|
||||
|
||||
### openInula 配套组件
|
||||
|
||||
#### 状态管理器 inula-X
|
||||
**状态管理器/inula-X**
|
||||
|
||||
inula-X 是 openInula 默认提供的状态管理器。无需额外引入三方库,就可以简单实现跨组件/页面共享状态。
|
||||
inula-X 是 openInula 默认提供的状态管理器,无需额外引入三方库,就可以简单实现跨组件/页面共享状态。
|
||||
inula-X 与 Redux 比可创建多个 Store,不需要在 Reducer 中返回 state 并且简化了 Action 和 Reducer 的创建步骤,原生支持异步能力,组件能做到精准重渲染。inula-X 均可使用函数组件、class 组件,能提供 redux 的适配接口及支持响应式的特点。
|
||||
|
||||
inula-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
|
||||
|
||||
* 微信公众号:
|
||||
|
||||

|
||||
|
|
|
@ -1,28 +1,3 @@
|
|||
# 0.0.2 版本
|
||||
|
||||
## 新特性
|
||||
|
||||
- **inula-request** 新增响应体中获取完整 URL 能力。
|
||||
|
||||
## API变更
|
||||
|
||||
无
|
||||
|
||||
## Bug修复
|
||||
|
||||
- **inula** 解决事件卸载失败问题。
|
||||
- **inula** 解决 mouseover 重复触发 mouseEnter 事件问题。
|
||||
- **inula** 大数组合并使用 concat。
|
||||
- **inula** 事件支持 defaultPrevented 属性
|
||||
|
||||
## CVE漏洞修复
|
||||
|
||||
无
|
||||
|
||||
## 已知问题
|
||||
|
||||
无
|
||||
|
||||
# 0.0.1 版本
|
||||
|
||||
## 新特性
|
||||
|
|
|
@ -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'],
|
||||
};
|
109
package.json
109
package.json
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
# openinula + vite
|
||||
|
||||
该模板提供了 `openinula` 工作在 `vite`的基础配置。
|
||||
> 请注意由于Vite插件有node版本限制,请使用`node -v`命令确认node版本大于等于node v18。
|
|
@ -11,7 +11,7 @@
|
|||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"openinula": "^0.1.1"
|
||||
"openinula": "^0.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.4",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"openinula": "^0.1.1"
|
||||
"openinula": "^0.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.4",
|
||||
|
|
|
@ -33,12 +33,10 @@ class App extends Inula.Component {
|
|||
</div>
|
||||
<div class="card animate__animated animate__zoomIn">
|
||||
<h2>了解更多</h2>
|
||||
<p>
|
||||
要了解 Inula,查看{' '}
|
||||
<a href="https://openinula.org" target="_blank">
|
||||
Inula 官网
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
要了解 Inula,查看{' '}
|
||||
<a href="https://openinula.org" target="_blank">Inula 官网</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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'],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,43 +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.
|
||||
*/
|
||||
|
||||
const BasicGenerator = require('../../BasicGenerator');
|
||||
|
||||
class Generator extends BasicGenerator {
|
||||
prompting() {
|
||||
return this.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'bundlerType',
|
||||
message: 'Please select the build type',
|
||||
choices: ['webpack', 'vite'],
|
||||
},
|
||||
]).then(props => {
|
||||
this.prompts = props;
|
||||
});
|
||||
}
|
||||
|
||||
writing() {
|
||||
const src = this.templatePath(this.prompts.bundlerType);
|
||||
const dest = this.destinationPath();
|
||||
this.writeFiles(src, dest, {
|
||||
context: {
|
||||
...this.prompts,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Generator;
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"description": "simple reactive app template."
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
# openinula + vite
|
||||
|
||||
该模板提供了 `openinula` 工作在 `vite`的基础配置。
|
||||
> 请注意由于Vite插件有node版本限制,请使用`node -v`命令确认node版本大于等于node v18。
|
|
@ -1,11 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>My Inula App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"name": "inula-vite-app",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"openinula": "0.0.0-experimental-20231201"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.4",
|
||||
"@babel/preset-env": "^7.21.4",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"@vitejs/plugin-react-refresh": "^1.3.6",
|
||||
"babel-plugin-import": "^1.13.6",
|
||||
"vite": "^4.2.1"
|
||||
}
|
||||
}
|
|
@ -1,38 +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, { useComputed, useReactive, useRef } from 'openinula';
|
||||
|
||||
function ReactiveComponent() {
|
||||
const renderCount = ++useRef(0).current;
|
||||
|
||||
const data = useReactive({ count: 0 });
|
||||
const countText = useComputed(() => {
|
||||
return `计时: ${data.count.get()}`;
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
data.count.set(c => c + 1);
|
||||
}, 1000);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>{countText}</div>
|
||||
<div>组件渲染次数:{renderCount}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReactiveComponent;
|
|
@ -1,57 +0,0 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #fff;
|
||||
background: linear-gradient(120deg, #6a11cb 0%, #2575fc 100%);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3em;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 1vh;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.card h2,
|
||||
.card p {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card a {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
|
@ -1,47 +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 from 'openinula';
|
||||
import ReactiveComponent from './ReactiveComponent';
|
||||
import './index.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div class="container">
|
||||
<div class="hero">
|
||||
<h1 class="hero-title animate__animated animate__bounceInDown">欢迎来到 Inula 项目!</h1>
|
||||
<p class="hero-subtitle animate__animated animate__bounceInUp">你已成功创建你的第一个响应式 Inula 项目</p>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="card animate__animated animate__zoomIn">
|
||||
<ReactiveComponent />
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="card animate__animated animate__zoomIn">
|
||||
<h2>了解更多</h2>
|
||||
<p>
|
||||
要了解 Inula,查看{' '}
|
||||
<a href="https://openinula.net/" target="_blank">
|
||||
Inula 官网
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Inula.render(<App />, document.getElementById('root'));
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"name": "inula-webpack-app",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "webpack serve --mode development",
|
||||
"build": "webpack --mode production"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"openinula": "0.0.0-experimental-20231201"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.4",
|
||||
"@babel/preset-env": "^7.21.4",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"babel-loader": "^9.1.2",
|
||||
"css-loader": "^6.7.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"style-loader": "^3.3.2",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.77.0",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.13.2"
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import Inula from 'openinula';
|
||||
import ReactiveComponent from './ReactiveComponent';
|
||||
import './styles.css';
|
||||
|
||||
class App extends Inula.Component {
|
||||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
<div class="hero">
|
||||
<h1 class="hero-title animate__animated animate__bounceInDown">欢迎来到 Inula 项目!</h1>
|
||||
<p class="hero-subtitle animate__animated animate__bounceInUp">你已成功创建你的第一个响应式 Inula 项目</p>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="card animate__animated animate__zoomIn">
|
||||
<ReactiveComponent />
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="card animate__animated animate__zoomIn">
|
||||
<h2>了解更多</h2>
|
||||
<p>
|
||||
要了解 Inula,查看{' '}
|
||||
<a href="https://openinula.org" target="_blank">
|
||||
Inula 官网
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -1,11 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Inula App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="../dist/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import Inula from 'openinula';
|
||||
import App from './App';
|
||||
|
||||
Inula.render(<App />, document.getElementById('root'));
|
|
@ -1,57 +0,0 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #fff;
|
||||
background: linear-gradient(120deg, #6a11cb 0%, #2575fc 100%);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3em;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 1vh;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.card h2,
|
||||
.card p {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card a {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
|
@ -1,84 +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.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: './src/index.jsx',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'bundle.js',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
'@babel/preset-env',
|
||||
[
|
||||
'@babel/preset-react',
|
||||
{
|
||||
runtime: 'automatic', // 新增
|
||||
importSource: 'openinula', // 新增
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
outputPath: 'images/',
|
||||
publicPath: 'images/',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(woff|woff2|eot|ttf|otf)$/,
|
||||
use: ['file-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(__dirname, 'src/index.html'),
|
||||
filename: 'index.html',
|
||||
}),
|
||||
],
|
||||
devServer: {
|
||||
static: path.join(__dirname, 'dist'),
|
||||
compress: true,
|
||||
port: 9000,
|
||||
open: true,
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'],
|
||||
},
|
||||
};
|
|
@ -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([
|
||||
{
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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声明规范
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
webpack/
|
||||
public/
|
|
@ -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)
|
||||
};
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -17,4 +17,4 @@
|
|||
|
||||
import run from '../lib/cli/cli.js';
|
||||
|
||||
run();
|
||||
run();
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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({
|
||||
|
|
|
@ -33,17 +33,15 @@ export default (api: API) => {
|
|||
args._.shift();
|
||||
}
|
||||
if (args._.length === 0) {
|
||||
api.logger.warn('Can\'t find any generate options.');
|
||||
api.logger.warn("Can't find any generate options.");
|
||||
return;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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引用
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
|
||||
export enum LogLevel {
|
||||
DEBUG = 0,
|
||||
INFO = 1,
|
||||
WARN = 2,
|
||||
INFO = 1,
|
||||
WARN = 2,
|
||||
ERROR = 3,
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}));
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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' });
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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%;
|
||||
}
|
|
@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 中存在空 index,nextItems 已经耗尽,不用处理
|
||||
// 确保新旧数组中 item 的 index 值不会发生变化
|
||||
this.lastRenderItemToIndexMap = nextRenderItemToIndexMap;
|
||||
return nextRenderItems;
|
||||
}
|
||||
}
|
|
@ -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%;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
|
@ -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}> 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);
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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' });
|
||||
});
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
},
|
||||
],
|
||||
// 请求某个节点的 props,hooks
|
||||
[
|
||||
RequestComponentAttrs,
|
||||
data => {
|
||||
parseCompAttrs(data);
|
||||
},
|
||||
],
|
||||
// 修改 props,hooks
|
||||
[
|
||||
ModifyAttrs,
|
||||
data => {
|
||||
modifyVNodeAttrs(data);
|
||||
},
|
||||
],
|
||||
// 找到节点对应 element
|
||||
[
|
||||
InspectDom,
|
||||
data => {
|
||||
inspectDom(data);
|
||||
},
|
||||
],
|
||||
// 打印节点数据
|
||||
[
|
||||
LogComponentData,
|
||||
data => {
|
||||
logComponentData(data);
|
||||
},
|
||||
],
|
||||
// 高亮
|
||||
[
|
||||
Highlight,
|
||||
data => {
|
||||
const node = queryVNode(data.id);
|
||||
if (node == null) {
|
||||
console.warn(`Could not find vNode with id "${data.id}"`);
|
||||
return null;
|
||||
}
|
||||
const info = getElement(helper.travelVNodeTree, node);
|
||||
showHighlight(info);
|
||||
},
|
||||
],
|
||||
// 移出高亮
|
||||
[
|
||||
RemoveHighlight,
|
||||
() => {
|
||||
hideHighlight();
|
||||
},
|
||||
],
|
||||
// 查看节点源代码位置
|
||||
[
|
||||
ViewSource,
|
||||
data => {
|
||||
const node = queryVNode(data.id);
|
||||
if (node == null) {
|
||||
console.warn(`Could not find vNode with id "${data.id}"`);
|
||||
return null;
|
||||
}
|
||||
showSource(node);
|
||||
},
|
||||
],
|
||||
// 选中页面元素对应 dev tools 节点
|
||||
[
|
||||
PickElement,
|
||||
() => {
|
||||
picker.startPick();
|
||||
},
|
||||
],
|
||||
[
|
||||
StopPickElement,
|
||||
() => {
|
||||
picker.stopPick();
|
||||
},
|
||||
],
|
||||
// 在控制台打印 Props Hooks State 值
|
||||
[
|
||||
CopyToConsole,
|
||||
data => {
|
||||
const node = queryVNode(data.id);
|
||||
if (node == null) {
|
||||
console.warn(`Could not find vNode with id "${data.id}"`);
|
||||
return null;
|
||||
}
|
||||
logDataWithPath(data.id, data.itemName, data.path, data.attrsName);
|
||||
},
|
||||
],
|
||||
// 把 Props Hooks State 值存为全局变量
|
||||
[
|
||||
StorageValue,
|
||||
data => {
|
||||
const node = queryVNode(data.id);
|
||||
if (node == null) {
|
||||
console.warn(`Could not find vNode with id "${data.id}"`);
|
||||
return null;
|
||||
}
|
||||
storeDataWithPath(data.id, data.path, data.attrsName);
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const showSource = (node: VNode) => {
|
||||
switch (node.tag) {
|
||||
case ClassComponent:
|
||||
case IncompleteClassComponent:
|
||||
case FunctionComponent:
|
||||
global.$type = node.type;
|
||||
break;
|
||||
case ForwardRef:
|
||||
global.$type = node.type.render;
|
||||
break;
|
||||
case MemoComponent:
|
||||
global.$type = node.type.type;
|
||||
break;
|
||||
default:
|
||||
global.$type = null;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRequest = (type: string, data) => {
|
||||
const action = actions.get(type);
|
||||
if (action) {
|
||||
action.call(this, data);
|
||||
return null;
|
||||
}
|
||||
console.warn('unknown command', type);
|
||||
};
|
||||
|
||||
function injectHook() {
|
||||
if ((window as any).__INULA_DEV_HOOK__) {
|
||||
return;
|
||||
}
|
||||
Object.defineProperty(window, '__INULA_DEV_HOOK__', {
|
||||
enumerable: false,
|
||||
value: {
|
||||
$0: null,
|
||||
init,
|
||||
isInit: false,
|
||||
addIfNotInclude,
|
||||
send,
|
||||
deleteVNode,
|
||||
// inulaX 使用
|
||||
getVNodeId: vNode => {
|
||||
return VNodeToIdMap.get(vNode);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
// 只接收我们自己的消息
|
||||
if (event.source !== window) {
|
||||
return;
|
||||
}
|
||||
const request = event.data;
|
||||
if (checkMessage(request, DevToolContentScript)) {
|
||||
const { payload } = request;
|
||||
const { type, data } = payload;
|
||||
|
||||
// 忽略 inulaX 的 actions
|
||||
if (type.startsWith('inulax')) {
|
||||
return;
|
||||
}
|
||||
handleRequest(type, data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
injectHook();
|
|
@ -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 };
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 的 name,userKey,indentation 属性不会发生变化
|
||||
// 但是在跳转到新页面时, id 值重置,此时原有 id 对应的节点都发生了变化,需要更新
|
||||
// 为了让架构尽可能简单,不区分是否是页面挑战,所以每次都需要重新赋值
|
||||
nextIdToTreeNodeMap[id] = lastItem;
|
||||
lastItem.name = name;
|
||||
lastItem.indentation = indentation;
|
||||
lastItem.userKey = userKey;
|
||||
data.push(lastItem);
|
||||
} else {
|
||||
const item = {
|
||||
id,
|
||||
name,
|
||||
indentation,
|
||||
userKey,
|
||||
};
|
||||
nextIdToTreeNodeMap[id] = item;
|
||||
data.push(item);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const getParents = (item: IData | null, parsedVNodeData: IData[]) => {
|
||||
const parents: IData[] = [];
|
||||
if (item) {
|
||||
const index = parsedVNodeData.indexOf(item);
|
||||
let indentation = item.indentation;
|
||||
for (let i = index; i >= 0; i--) {
|
||||
const last = parsedVNodeData[i];
|
||||
const lastIndentation = last.indentation;
|
||||
if (lastIndentation < indentation) {
|
||||
parents.push(last);
|
||||
indentation = lastIndentation;
|
||||
}
|
||||
}
|
||||
}
|
||||
return parents;
|
||||
};
|
||||
|
||||
interface IIdToNodeMap {
|
||||
[id: number]: IData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 dev tools 页面左树占比
|
||||
*
|
||||
* @param {null | HTMLElement} resizeElement 要改变宽度的页面元素
|
||||
* @param {number} percentage 宽度占比
|
||||
*/
|
||||
const setResizePCTForElement = (resizeElement: null | HTMLElement, percentage: number): void => {
|
||||
if (resizeElement !== null) {
|
||||
resizeElement.style.setProperty('--horizontal-percentage', `${percentage}`);
|
||||
}
|
||||
};
|
||||
|
||||
function resizeReducer(state: ResizeState, action: ResizeAction): ResizeState {
|
||||
switch (action.type) {
|
||||
case 'START_RESIZE':
|
||||
return {
|
||||
...state,
|
||||
isResizing: action.payload,
|
||||
};
|
||||
case 'SET_HORIZONTAL_PERCENTAGE':
|
||||
return {
|
||||
...state,
|
||||
horizontalPercentage: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function initResizeState(): ResizeState {
|
||||
const horizontalPercentage = 0.62;
|
||||
|
||||
return {
|
||||
horizontalPercentage,
|
||||
isResizing: false,
|
||||
};
|
||||
}
|
||||
|
||||
function Panel({ viewSource, inspectVNode }) {
|
||||
const [parsedVNodeData, setParsedVNodeData] = useState([]);
|
||||
const [componentAttrs, setComponentAttrs] = useState<{
|
||||
parsedProps?: IAttr[];
|
||||
parsedState?: IAttr[];
|
||||
parsedHooks?: IAttr[];
|
||||
}>({});
|
||||
const [selectComp, setSelectComp] = useState<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);
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import Panel from './Panel';
|
||||
|
||||
// 这里导出 Panel 为了加载 Panel.less
|
||||
export default Panel;
|
|
@ -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>
|
|
@ -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';
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue