+ );
+}
+
+Inula.render(, document.getElementById('root'));
diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/vite.config.js b/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/vite.config.js
new file mode 100644
index 00000000..43065ece
--- /dev/null
+++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/vite/vite.config.js
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+import react from '@vitejs/plugin-react';
+
+let alias = {
+ react: 'openinula', // 新增
+ 'react-dom': 'openinula', // 新增
+ 'react/jsx-dev-runtime': 'openinula/jsx-dev-runtime',
+};
+
+export default {
+ plugins: [react()],
+ resolve: {
+ alias,
+ },
+};
diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/package.json b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/package.json
new file mode 100644
index 00000000..783f626a
--- /dev/null
+++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "inula-webpack-app",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "start": "webpack serve --mode development",
+ "build": "webpack --mode production"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "openinula": "0.0.0-experimental-20231201"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.21.4",
+ "@babel/preset-env": "^7.21.4",
+ "@babel/preset-react": "^7.18.6",
+ "babel-loader": "^9.1.2",
+ "css-loader": "^6.7.3",
+ "file-loader": "^6.2.0",
+ "html-webpack-plugin": "^5.5.0",
+ "style-loader": "^3.3.2",
+ "url-loader": "^4.1.1",
+ "webpack": "^5.77.0",
+ "webpack-cli": "^5.0.1",
+ "webpack-dev-server": "^4.13.2"
+ }
+}
diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/App.jsx b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/App.jsx
new file mode 100644
index 00000000..5a294011
--- /dev/null
+++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/App.jsx
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+import Inula from 'openinula';
+import ReactiveComponent from './ReactiveComponent';
+import './styles.css';
+
+class App extends Inula.Component {
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default App;
diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/ReactiveComponent.jsx b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/ReactiveComponent.jsx
new file mode 100644
index 00000000..cca83f05
--- /dev/null
+++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/ReactiveComponent.jsx
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+import Inula, { useComputed, useReactive, useRef } from 'openinula';
+
+function ReactiveComponent() {
+ const renderCount = ++useRef(0).current;
+
+ const data = useReactive({ count: 0 });
+ const countText = useComputed(() => {
+ return `计时: ${data.count.get()}`;
+ });
+
+ setInterval(() => {
+ data.count.set(c => c + 1);
+ }, 1000);
+
+ return (
+
+
{countText}
+
组件渲染次数:{renderCount}
+
+ );
+}
+
+export default ReactiveComponent;
diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.html b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.html
new file mode 100644
index 00000000..c966e725
--- /dev/null
+++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ Inula App
+
+
+
+
+
+
diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.jsx b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.jsx
new file mode 100644
index 00000000..c384f05a
--- /dev/null
+++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/index.jsx
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+import Inula from 'openinula';
+import App from './App';
+
+Inula.render(, document.getElementById('root'));
diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/styles.css b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/styles.css
new file mode 100644
index 00000000..f08fdb8a
--- /dev/null
+++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/src/styles.css
@@ -0,0 +1,57 @@
+* {
+ box-sizing: border-box;
+}
+
+body,
+html {
+ margin: 0;
+ padding: 0;
+ font-family: 'Montserrat', sans-serif;
+ line-height: 1.6;
+ color: #fff;
+ background: linear-gradient(120deg, #6a11cb 0%, #2575fc 100%);
+ height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.container {
+ text-align: center;
+}
+
+.hero-title {
+ font-size: 3em;
+ margin-bottom: 20px;
+}
+
+.hero-subtitle {
+ font-size: 1.5em;
+ margin-bottom: 50px;
+}
+
+.content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin: 1vh;
+}
+
+.card {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 5px;
+ padding: 20px;
+ width: 100%;
+ box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(4px);
+}
+
+.card h2,
+.card p {
+ color: #fff;
+}
+
+.card a {
+ color: #fff;
+ text-decoration: underline;
+}
diff --git a/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/webpack.config.js b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/webpack.config.js
new file mode 100644
index 00000000..8b46f7a5
--- /dev/null
+++ b/packages/create-inula/lib/generators/Simple-reactive-app/templates/webpack/webpack.config.js
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+const path = require('path');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+
+module.exports = {
+ entry: './src/index.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'],
+ },
+};
diff --git a/packages/create-inula/lib/run.js b/packages/create-inula/lib/run.js
index 15099589..268022ed 100644
--- a/packages/create-inula/lib/run.js
+++ b/packages/create-inula/lib/run.js
@@ -62,7 +62,17 @@ const run = async config => {
}
process.emit('message', { type: 'prompt' });
- let { type } = config;
+ let { type, name } = config;
+ if (!name) {
+ const answers = await inquirer.prompt([
+ {
+ name: 'projectName',
+ message: 'Project name',
+ type: 'input'
+ },
+ ]);
+ config.name = answers.projectName;
+ }
if (!type) {
const answers = await inquirer.prompt([
{
diff --git a/packages/create-inula/package.json b/packages/create-inula/package.json
index 5cea2800..251ecd60 100644
--- a/packages/create-inula/package.json
+++ b/packages/create-inula/package.json
@@ -1,11 +1,14 @@
{
"name": "create-inula",
- "version": "0.0.2",
+ "version": "0.0.8",
"description": "",
"main": "index.js",
"bin": {
"create-inula": "bin/cli.js"
},
+ "engines": {
+ "node": ">= 18.0.0"
+ },
"files": [
"bin",
"lib",
diff --git a/packages/inula-cli/README.md b/packages/inula-cli/README.md
index 5e26db29..bebd176e 100644
--- a/packages/inula-cli/README.md
+++ b/packages/inula-cli/README.md
@@ -2,23 +2,23 @@
## 一、安装使用
-### 安装Nodejs
+### 安装Node.js
-inula-cli的运行需要依赖Nodejs,使用前请确保您的电脑已安装Nodejs,并且版本在16以上。您可以通过在控制台执行以下命令来确认您的版本。
+inula-cli的运行需要依赖Node.js,使用前请确保您的电脑已安装Node.js,并且版本在16以上。您可以通过在控制台执行以下命令来确认您的版本。
-```
+```shell
>node -v
v16.4.0
```
-如果您没有安装Nodejs,或者Nodejs版本不满足条件,推荐使用nvm工具安装和管理Nodejs版本。
+如果您没有安装Node.js,或者Node.js版本不满足条件,推荐使用nvm工具安装和管理Node.js版本。
nvm最新版本下载: [https://github.com/coreybutler/nvm-windows/releases](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fcoreybutler%2Fnvm-windows%2Freleases)
-安装nvm之后,可以通过如下命令安装Nodejs:
+安装nvm之后,可以通过如下命令安装Node.js:
-```
+```shell
>node install 16
>node use 16
@@ -28,15 +28,15 @@ nvm最新版本下载: [https://github.com/coreybutler/nvm-windows/releases](htt
### 安装inula-cli
-为了方便使用inula-cli的功能,推荐您全局安装inula-cli。Nodejs安装会自带npm工具用于管理模块,您可以直接运行如下命令:
+为了方便使用inula-cli的功能,推荐您全局安装inula-cli。Node.js安装会自带npm工具用于管理模块,您可以直接运行如下命令:
-```
+```shell
>npm install -g inula-cli
```
安装完成后,使用inula-cli version命令确认安装是否完成。
-```
+```shell
>inula-cli version
1.1.0
```
@@ -93,7 +93,7 @@ inula-cli支持用户通过项目根目录下的.inula.ts或者.inula.js文件
在配置文件中,您需要默认导出一个配置,以下为一个简单的配置文件示例:
-```
+```typescript
// .inula.ts
export default {
@@ -113,7 +113,7 @@ export default {
对于TypeScript类型,我们也提供了类型定义以供开发时自动补全:
-```
+```typescript
// .inula.ts
import { defineConfig } from "inula-cli"
@@ -164,8 +164,8 @@ inula-cli的所有功能都围绕插件展开,插件可以很方便地让用
内置插件在inula-cli运行时会自动加载,用户可以直接调用这些内置命令,当前支持的内置插件功能如下:
-| 序号 | 插件功能 | 触发命令 |
-| ---- | :----------------------- | ------------------- |
+| 序号 | 插件功能 | 触发命令 |
+| ---- | :----------------------- | ----------------- |
| 1、 | 本地开发构建 | inula-cli dev |
| 2、 | 生产构建 | inula-cli build |
| 3、 | 接口mock能力 | inula-cli dev |
@@ -180,13 +180,13 @@ inula-cli支持用户集成已发布在npm仓库的插件,用户可以按需
安装可以通过npm安装,这里以插件@inula/add为例:
-```
+```shell
npm i --save-dev @inula/add
```
如果需要运行插件,需要在配置文件中配置对应的插件路径
-```
+```typescript
// .inula.ts
export default {
@@ -205,7 +205,7 @@ export default {
1、编写命令插件文件,这里我们自定义了一个conf命令用于展示当前项目的配置信息。
-```
+```typescript
// conf.ts
import { API } from "inula-cli";
@@ -224,7 +224,7 @@ export default (api: API) => {
2、在配置文件中加入对插件的引用
-```
+```typescript
// .inula.ts
export default {
@@ -232,9 +232,9 @@ export default {
}
```
-3、在项目根目录下执行inula-cli conf即可触发插件运行。
+3、在项目根目录下执行`inula-cli conf`即可触发插件运行。
-```
+```shell
> inula-cli conf
current user config is: {
plugins: [ './conf', './showConf' ],
@@ -247,7 +247,7 @@ inula-cli提供了hook机制可以让开发者在执行命令时实现事件监
1、使用插件注册hook
-```
+```typescript
// modifyConfig.ts
import { API } from "inula-cli";
@@ -267,7 +267,7 @@ export default (api: API) => {
2、在插件中触发hook
-```
+```typescript
// conf.ts
import { API } from "inula-cli";
@@ -287,7 +287,7 @@ export default (api: API) => {
3、在配置文件中加入插件
-```
+```typescript
// .inula.ts
export default {
@@ -297,7 +297,7 @@ export default {
4、触发命令
-```
+```shell
> inula-cli conf
current user config is: {
plugins: [ './conf', './showConf' ],
@@ -327,7 +327,7 @@ current user config is: {
registerCommand方法允许用户自定义inula-cli的执行命令,
-```
+```typescript
api.registerCommand({
name: string,
description?: string,
@@ -343,7 +343,7 @@ registerCommand方法允许用户自定义inula-cli的执行命令,
使用示例:
-```
+```typescript
import { API } from "inula-cli";
export default (api: API) => {
@@ -374,7 +374,7 @@ api.registerHook({
使用示例:
-```
+```typescript
import { API } from "inula-cli";
export default (api: API) => {
@@ -403,7 +403,7 @@ applyHook(name: string, value?: any})
使用示例:
-```
+```typescript
import { API } from "inula-cli";
export default (api: API) => {
@@ -427,7 +427,7 @@ export default (api: API) => {
inula-cli默认集成生产构建能力,用户可以通过在.inula.ts中配置buildConfig字段启用功能。配置示例如下:
-```
+```typescript
// .inula.ts
// 使用webpack构建
@@ -460,7 +460,7 @@ export default {
生产构建支持传入多个配置文件路径,使用webpack构建还支持配置文件以函数方式导出,inula-cli会将配置中的env和args作为参数传递到函数中执行以获取最后的构建配置。
-```
+```typescript
// webpack.config.js
module.exports = function (env, argv) {
@@ -497,7 +497,7 @@ export default {
inula-cli默认也支持项目本地构建,用户可以通过在.inula.ts中配置devBuildConfig字段启用功能。配置示例如下:
-```
+```typescript
// .inula.ts
// 使用webpack构建
@@ -552,7 +552,7 @@ inula-cli自动将项目根路径里/Mock目录下所有文件视为mock文件
如果您想修改Mock目录位置,可以在配置文件中修改mockPath。如果不配置该参数,默认使用"./mock"。
-```
+```typescript
// .inula.ts
export default {
...
@@ -569,7 +569,7 @@ export default {
Mock文件需要默认导出一个对象,key为"请求方式 接口名",值为接口实现。示例如下:
-```
+```typescript
export default {
"GET /api/user": (req, res) => {
res.status(200).json("admin")
@@ -579,7 +579,7 @@ export default {
如果想要一次mock多个接口,可以在导出对象中设置多个key,例如:
-```
+```typescript
export default {
"GET /api/user": (req, res) => {
res.status(200).json("admin");
@@ -597,7 +597,7 @@ export default {
Mock文件默认导出一个数组,数组每一个成员示例如下:
-```
+```typescript
export default [
{
url: '/api/get',
@@ -654,7 +654,7 @@ export default [
在框架配置文件中,开发者需要配置远端服务器地址以及编写自定义的matcher函数提供给框架:
-```
+```typescript
// .inula.ts
const matcher = (pathname, request) => {
@@ -689,7 +689,7 @@ export default {
用户可以在.inula.ts中配置remoteProxy字段开启远端静态接口代理能力,完成配置后,使用后执行inula-cli proxy启动该功能。
-```
+```typescript
// .inula.ts
export default {
@@ -710,6 +710,3 @@ export default {
}
}
```
-
-
-
diff --git a/packages/inula-cli/src/types/types.ts b/packages/inula-cli/src/types/types.ts
index 222c8e05..23bde194 100644
--- a/packages/inula-cli/src/types/types.ts
+++ b/packages/inula-cli/src/types/types.ts
@@ -165,4 +165,3 @@ export interface Arguments {
'--'?: Array;
[argName: string]: any;
}
-
diff --git a/packages/inula-dev-tools/README.md b/packages/inula-dev-tools/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/inula-dev-tools/babel.config.js b/packages/inula-dev-tools/babel.config.js
new file mode 100644
index 00000000..10a19b67
--- /dev/null
+++ b/packages/inula-dev-tools/babel.config.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+module.exports = api => {
+ const isTest = api.env('test');
+ console.log('isTest', isTest);
+
+ const plugins = [
+ ['@babel/plugin-proposal-class-properties', { loose: false }],
+ ];
+
+ if (process.env.NODE_ENV !== 'production') {
+ plugins.push(['@babel/plugin-transform-react-jsx-source']);
+ }
+
+ return {
+ presets: [
+ '@babel/preset-env',
+ '@babel/preset-typescript',
+ [
+ '@babel/preset-react', {
+ runtime: 'classic',
+ 'pragma': 'Inula.createElement',
+ 'pragmaFrag': 'Inula.Fragment',
+ }]
+ ],
+ plugins,
+ };
+};
diff --git a/packages/inula-dev-tools/externals.d.ts b/packages/inula-dev-tools/externals.d.ts
new file mode 100644
index 00000000..72bdaf9d
--- /dev/null
+++ b/packages/inula-dev-tools/externals.d.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+declare module '*.less' {
+ const resource: {[key: string]: string};
+ export = resource;
+}
diff --git a/packages/inula-dev-tools/global.d.ts b/packages/inula-dev-tools/global.d.ts
new file mode 100644
index 00000000..646c0399
--- /dev/null
+++ b/packages/inula-dev-tools/global.d.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+/*
+ 区分是否开发者模式
+ */
+declare var isDev: boolean;
+declare var isTest: boolean;
+declare const __VERSION__: string;
diff --git a/packages/inula-dev-tools/package.json b/packages/inula-dev-tools/package.json
new file mode 100644
index 00000000..46f655f8
--- /dev/null
+++ b/packages/inula-dev-tools/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "inula-dev-tools",
+ "version": "0.0.1",
+ "description": "Inula chrome dev extension",
+ "main": "",
+ "scripts": {
+ "build": "webpack --config ./webpack.config.js",
+ "watch": "webpack --config ./webpack.config.js --watch",
+ "build-dev": "webpack --config ./webpack.dev.js",
+ "start": "npm run build && webpack serve --config ./webpack.dev.js",
+ "test": "jest"
+ },
+ "keywords": ["inula-dev-tools"],
+ "license": "MulanPSL2",
+ "devDependencies": {
+ "@babel/core": "^7.12.3",
+ "@babel/plugin-proposal-class-properties": "^7.16.7",
+ "@babel/plugin-transform-react-jsx-source": "^7.18.6",
+ "@babel/preset-env": "^7.21.1",
+ "@babel/preset-react": "^7.12.1",
+ "@babel/preset-typescript": "^7.16.7",
+ "@types/chrome": "0.0.190",
+ "@types/jest": "27.4.1",
+ "@typescript-eslint/eslint-plugin": "4.8.0",
+ "@typescript-eslint/parser": "4.8.0",
+ "babel-jest": "^27.5.1",
+ "eslint": "7.13.0",
+ "eslint-config-prettier": "^6.9.0",
+ "eslint-plugin-jest": "^22.15.0",
+ "eslint-plugin-no-for-of-loops": "^1.0.0",
+ "eslint-plugin-no-function-declare-after-return": "^1.0.0",
+ "eslint-plugin-react": "7.14.3",
+ "babel-loader": "8.1.0",
+ "css-loader": "^6.7.1",
+ "html-webpack-plugin": "5.5.0",
+ "jest": "27.5.1",
+ "less": "4.1.2",
+ "less-loader": "10.2.0",
+ "style-loader": "^3.3.1",
+ "ts-jest": "27.1.4",
+ "ts-loader": "^9.3.1",
+ "typescript": "4.2.3",
+ "webpack": "5.70.0",
+ "webpack-cli": "4.9.2",
+ "webpack-dev-server": "^4.7.4"
+ },
+ "dependencies": {
+ "openinula": "^0.1.1",
+ "flatted-object": "^0.1.2",
+ "json-decycle": "^2.0.1",
+ "lodash": "^4.17.21",
+ "object-assign": "^4.1.1"
+ }
+}
diff --git a/packages/inula-dev-tools/src/background/index.ts b/packages/inula-dev-tools/src/background/index.ts
new file mode 100644
index 00000000..0efe312b
--- /dev/null
+++ b/packages/inula-dev-tools/src/background/index.ts
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+import { checkMessage, packagePayload, changeSource } from "../utils/transferUtils";
+import { RequestAllVNodeTreeInfos, InitDevToolPageConnection, DevToolBackground } from "../utils/constants";
+import { DevToolPanel, DevToolContentScript } from "../utils/constants";
+
+// 多个页面 tab 页共享一个 background,需要建立连接池,给每个 tab 建立连接
+export const connections = {};
+
+// panel 代码中调用 let backgroundPageConnection = chrome.runtime.connect({...}) 会触发回调函数
+chrome.runtime.onConnect.addListener(function (port) {
+ function extensionListener(message) {
+ const isInulaMessage = checkMessage(message, DevToolPanel);
+ if (isInulaMessage) {
+ const { payload } = message;
+ // tabId 值指当前浏览器分配给 web_page 的 id 值。是 panel 页面查询得到,指定向页面发送消息
+ const { type, data, tabId } = payload;
+ let passMessage;
+ if (type === InitDevToolPageConnection) {
+ // 记录 panel 所在 tab 页的 tabId,如果已经记录了,覆盖原有 port,因为原有 port 可能关闭了
+ // 可能这次是 panel 发起的重新建立请求
+ connections[tabId] = port;
+ passMessage = packagePayload({ type: RequestAllVNodeTreeInfos }, DevToolBackground);
+ } else {
+ passMessage = packagePayload({ type, data }, DevToolBackground);
+ }
+ chrome.tabs.sendMessage(tabId, passMessage);
+ }
+ }
+
+ // 监听 dev tools 页面发送的消息
+ port.onMessage.addListener(extensionListener);
+
+ port.onDisconnect.addListener(function (port) {
+ port.onMessage.removeListener(extensionListener);
+
+ const tabs = Object.keys(connections);
+ for (let i = 0, len = tabs.length; i < len; i++) {
+ if (connections[tabs[i]] == port) {
+ delete connections[tabs[i]];
+ break;
+ }
+ }
+ });
+});
+
+// 监听来自 content script 的消息,并将消息发送给对应的 dev tools 页面
+chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
+ // 来自 content script 的消息需要预先设置 sender.tab
+ if (sender.tab) {
+ const tabId = sender.tab.id;
+ if (message.payload.type.startsWith('inulax')) {
+ return;
+ }
+ if (tabId && tabId in connections && checkMessage(message, DevToolContentScript)) {
+ changeSource(message, DevToolBackground);
+ connections[tabId].postMessage(message);
+ } else {
+ // TODO: 如果查询失败,发送 chrome message ,请求 panel 主动建立连接
+ console.log('Tab is not found in connection');
+ }
+ } else {
+ console.log('sender.tab is not defined');
+ }
+ // 需要返回消息告知完成通知,否则会出现报错 message port closed before a response was received
+ sendResponse({ status: 'ok' });
+});
diff --git a/packages/inula-dev-tools/src/background/inulaXHandler.ts b/packages/inula-dev-tools/src/background/inulaXHandler.ts
new file mode 100644
index 00000000..4c7e9c2b
--- /dev/null
+++ b/packages/inula-dev-tools/src/background/inulaXHandler.ts
@@ -0,0 +1,296 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+import { DevToolBackground, DevToolContentScript, DevToolPanel } from '../utils/constants';
+import { connections } from './index';
+import { packagePayload } from '../utils/transferUtils';
+
+// 监听来自 content script 的消息,并将消息发送给对应的 dev tools page
+const eventsPerTab = {};
+const storesPerTab = {};
+const observedComponents = {};
+const eventPersistencePerTab = {};
+let idGenerator = 1;
+
+// 当 tab 重新加载,需要对该 tab 所监听的 stores 进行重置
+chrome.tabs.onUpdated.addListener(function (tabId, changeInfo) {
+ if (changeInfo.status === 'loading') {
+ if (!eventPersistencePerTab[tabId]) {
+ eventsPerTab[tabId] = [];
+ }
+ storesPerTab[tabId] = [];
+ }
+});
+
+function sendTo(connectionId, message) {
+ if (connections[connectionId]) {
+ connections[connectionId].postMessage(message);
+ }
+}
+
+function requestObservedComponents(tabId) {
+ setTimeout(() => {
+ chrome.tabs.sendMessage(
+ tabId,
+ packagePayload(
+ {
+ type: 'inulax request observed components',
+ data: {}
+ },
+ 'dev tool background'
+ )
+ );
+ }, 1);
+}
+
+function executeAction(tabId, storeId, action, params) {
+ chrome.tabs.sendMessage(
+ tabId,
+ packagePayload(
+ {
+ type: 'inulax execute action',
+ data: {
+ action,
+ storeId,
+ params,
+ }
+ },
+ 'dev tool background'
+ )
+ );
+}
+
+function queueAction(tabId, storeId, action, params) {
+ chrome.tabs.sendMessage(
+ tabId,
+ packagePayload(
+ {
+ type: 'inulax queue action',
+ data: {
+ action,
+ storeId,
+ params,
+ }
+ },
+ 'sev tool background'
+ )
+ );
+}
+
+function getObservedComponents(storeId, tabId) {
+ if (!observedComponents[tabId]) {
+ observedComponents[tabId] = {};
+ }
+ if (!observedComponents[tabId][storeId]) {
+ return [];
+ }
+ return observedComponents[tabId][storeId];
+}
+
+// 来自 content script 的消息
+chrome.runtime.onMessage.addListener(function (message, sender) {
+ if (message.payload.type.startsWith('inulax')) {
+ console.log('inulaXHandler message from content script', {
+ payload: { ...message.payload },
+ });
+
+ if (message.from === DevToolContentScript && sender.tab?.id) {
+ if (message.payload.type === 'inulax observed components') {
+ observedComponents[sender.tab.id] = message.payload.data;
+
+ sendTo(sender.tab.id, {
+ type: 'INULA_DEV_TOOLS',
+ payload: {
+ type: 'inulax observed components',
+ data: message.payload.data,
+ },
+ from: DevToolBackground,
+ });
+ return;
+ }
+ requestObservedComponents(sender.tab.id);
+
+ // content script -> inulaXHandler
+ if (!eventsPerTab[sender.tab.id]) {
+ eventsPerTab[sender.tab.id] = [];
+ }
+ eventsPerTab[sender.tab.id].push({
+ id: idGenerator++,
+ timestamp: Date.now(),
+ message: message.payload,
+ });
+
+ sendTo(sender.tab.id, {
+ type: 'INULA_DEV_TOOLS',
+ payload: {
+ type: 'inulax events',
+ events: eventsPerTab[sender.tab.id],
+ },
+ from: DevToolBackground,
+ });
+
+ // 如果当前 tab 没有 store data,则初始化
+ if (!storesPerTab[sender.tab.id]) {
+ storesPerTab[sender.tab.id] = [];
+ }
+
+ let found = false;
+ storesPerTab[sender.tab.id]?.some((store, index) => {
+ if (store.id === message.payload.data.store.id) {
+ found = true;
+ storesPerTab[sender.tab!.id!][index] = message.payload.data.store;
+ requestObservedComponents(sender.tab?.id);
+ return true;
+ }
+ return false;
+ });
+
+ if (!found) {
+ const tabId = sender.tab.id;
+ if (!storesPerTab[tabId]) {
+ storesPerTab[tabId] = [];
+ }
+ storesPerTab[tabId].push(message.payload.data.store);
+ sendTo(tabId, {
+ type: 'INULA_DEV_TOOLS',
+ payload: {
+ type: 'inulax stores',
+ stores: storesPerTab[tabId]?.map(store => {
+ // 连接被监测的组件
+ requestObservedComponents(tabId);
+ const observedComponents = getObservedComponents(store, tabId);
+ return { ...store, observedComponents };
+ }) || [],
+ newStore: message.payload.data.store.id,
+ },
+ from: DevToolBackground,
+ });
+ return;
+ }
+
+ sendTo(sender.tab.id, {
+ type: 'INULA_DEV_TOOLS',
+ payload: {
+ type: 'inulax stores',
+ stores: storesPerTab[sender.tab.id]?.map(store => {
+ // 连接被监测的组件
+ const observedComponents = getObservedComponents(store, sender.tab?.id);
+ return { ...store, observedComponents };
+ }) || [],
+ updated: message.payload.data.store.id,
+ },
+ from: DevToolBackground,
+ });
+
+ requestObservedComponents(message.payload.tabId);
+ }
+
+ if (message.from === DevToolPanel) {
+ // panel -> inulaXHandler
+ if (message.payload.type === 'inulax run action') {
+ executeAction(
+ message.payload.tabId,
+ message.payload.storeId,
+ message.payload.action,
+ message.payload.args
+ );
+ return;
+ }
+
+ if (message.payload.type === 'inulax change state') {
+ chrome.tabs.sendMessage(
+ message.payload.tabId,
+ packagePayload(message.payload, 'dev tool background')
+ );
+ return;
+ }
+
+ if (message.payload.type === 'inulax queue action') {
+ queueAction(
+ message.payload.tabId,
+ message.payload.storeId,
+ message.payload.action,
+ message.payload.args
+ );
+ return;
+ }
+
+ if (message.payload.type === 'inulax resetEvents') {
+ eventsPerTab[message.payload.tabId] = [];
+ sendTo(message.payload.tabId, {
+ type: 'INULA_DEV_TOOLS',
+ payload: {
+ type: 'inulax events',
+ events: eventsPerTab[message.payload.tabId],
+ },
+ from: DevToolBackground,
+ });
+ return;
+ }
+
+ if (message.payload.type === 'inula setPersistent'){
+ const { tabId, persistent } = message.payload;
+ eventPersistencePerTab[tabId] = persistent;
+ return;
+ }
+
+ if (message.payload.type === 'inulax getPersistence') {
+ sendTo(message.payload.tabId, {
+ type: 'INULA_DEV_TOOLS',
+ payload: {
+ type: 'inulax persistence',
+ persistent: !!eventPersistencePerTab[message.payload.tabId],
+ },
+ from: DevToolBackground,
+ });
+ return;
+ }
+
+ if (message.payload.type === 'inulax getEvents') {
+ if (!eventsPerTab[message.payload.tabId]) {
+ eventsPerTab[message.payload.tabId] = [];
+ }
+ sendTo(message.payload.tabId, {
+ type: 'INULA_DEV_TOOLS',
+ payload: {
+ type: 'inulax events',
+ events: eventsPerTab[message.payload.tabId],
+ },
+ from: DevToolBackground,
+ });
+ return;
+ }
+
+ if (message.payload.type === 'inulax getStores') {
+ sendTo(message.payload.tabId, {
+ type: 'INULA_DEV_TOOLS',
+ payload: {
+ type: 'inulax stores',
+ stores: storesPerTab[message.payload.tabId]?.map(store => {
+ requestObservedComponents(message.payload.tabId);
+ const observedComponents = getObservedComponents(
+ store.id,
+ message.payload.tabId
+ );
+ return { ...store, observedComponents };
+ }) || [],
+ },
+ from: DevToolBackground,
+ });
+ return;
+ }
+ }
+ }
+});
diff --git a/packages/inula-dev-tools/src/components/ComponentInfo.less b/packages/inula-dev-tools/src/components/ComponentInfo.less
new file mode 100644
index 00000000..d8b8f432
--- /dev/null
+++ b/packages/inula-dev-tools/src/components/ComponentInfo.less
@@ -0,0 +1,279 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+@import 'assets.less';
+
+.infoContainer {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ .button {
+ border: none;
+ padding: 0;
+ border-radius: 0.25rem;
+ flex: 0 0 auto;
+ cursor: pointer;
+ color: #5f6673;
+ }
+
+ .button :hover {
+ color: #23272f;
+ }
+
+ .componentInfoHead {
+ flex: 0 0 @top-height;
+ display: flex;
+ align-items: center;
+ border-bottom: @divider-style;
+
+ .name {
+ flex: 1 1 auto;
+ padding: 0 1rem 0 1rem;
+ .text {
+ display: block;
+ }
+ }
+
+ .eye {
+ flex: 0 0 1rem;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ padding: 0.25rem 0.5rem 0.25rem 0.25rem;
+ }
+
+ .debug {
+ flex: 0 0 1rem;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ padding: 0.25rem 0.5rem 0.25rem 0;
+ }
+
+ .location {
+ flex: 0 0 1rem;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ padding: 0.25rem 0.5rem 0.25rem 0;
+ }
+ }
+
+ .componentInfoMain {
+ overflow-y: auto;
+
+ > :last-child {
+ border-bottom: unset;
+ }
+
+ > div {
+ border-bottom: @divider-style;
+ }
+
+ .attrContainer {
+ flex: 0 0;
+
+ .attrHead {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ padding: 0.5rem 0.5rem 0 0.5rem;
+
+ .attrType {
+ flex: 1 1 0;
+ }
+
+ .attrCopy {
+ flex: 0 0 1rem;
+ padding-right: 1rem;
+ }
+ }
+
+ .attrDetail {
+ padding-bottom: 0.5rem;
+
+ .attrArrow {
+ color: @arrow-color;
+ width: 12px;
+ display: inline-block;
+ }
+
+ .attrName {
+ margin-top: 1px;
+ color: @attr-name-color;
+ font-family: @attr-name-font-family;
+ }
+
+ .colon {
+ margin-top: 1px;
+ transform: translateY(-8%);
+ margin-right: 0.5rem;
+ }
+
+ .info {
+ display: flex;
+ &:hover {
+ .operation {
+ visibility: visible;
+ .operationIcon :hover {
+ border: none;
+ border-radius: 5px;
+ background-color: lightskyblue;
+ }
+ }
+ }
+ }
+
+ .attrValue {
+ width: 26rem;
+ height: 1rem;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo,
+ Courier, monospace;
+ &:focus {
+ color: unset;
+ background-color: #f0f0f0;
+ }
+ }
+ .attrValue[data-type='string'] {
+ color: #009906;
+ }
+ .attrValue[data-type='function'] {
+ color: royalblue;
+ }
+ .attrValue[data-type='number'] {
+ color: #ff5722;
+ }
+ .attrValue[data-type='boolean'] {
+ color: #03a9f4;
+ }
+
+ .operation {
+ cursor: pointer;
+ visibility: hidden;
+ }
+
+ .checkBox {
+ margin: 2px 3px 0 auto;
+ justify-content: flex-end;
+ }
+ }
+ }
+
+ .dropdown.active {
+ display: unset;
+ top: var(--content-top);
+ left: var(--content-left);
+ position: absolute;
+ ul {
+ margin-block-start: 0;
+ padding-inline-start: 0;
+ li {
+ padding: 10px;
+ border-top: 1px lighten(#333, 2%) solid;
+ height: auto;
+ overflow: auto;
+ opacity: 1;
+ }
+ }
+ }
+
+ .dropdown {
+ display: none;
+
+ ul {
+ display: block;
+ position: relative;
+ list-style: none;
+ }
+
+ li {
+ padding: 0 10px;
+ background: darken(#333, 2%);
+ color: darken(#EEE, 40%);
+ text-align: left;
+ border: 0;
+ width: 100%;
+ height: 0;
+ overflow: hidden;
+ cursor: pointer;
+ opacity: 0;
+ transition-property: all, background-color;
+ transition-duration: 0.2s, 0.4s;
+
+ &:hover, &.selected {
+ background-color: darken(#333, 10%);
+ }
+
+ &:active {
+ background: #03a9f4;
+ }
+
+ &:first-child {
+ border-radius: 5px 5px 0 0;
+ }
+
+ &:last-child {
+ border-radius: 0 0 5px 5px;
+ }
+
+ &:before {
+ margin-top: -2px;
+ margin-right: 10px;
+ display: inline-block;
+ border-radius: 5px;
+ vertical-align: middle;
+ width: 16px;
+ height: 16px;
+ }
+
+ &:nth-child(1) {
+ &:before {
+ content: url('../svgs/copy.svg');
+ }
+ }
+
+ &:nth-child(2) {
+ &:before {
+ content: url('../svgs/storage.svg');
+ }
+ }
+ }
+ }
+ }
+
+ .parentsInfo {
+ flex: 1 1 0;
+
+ .parentName {
+ padding: 0.5rem 0.5rem 0 0.5rem;
+ }
+
+ .parent {
+ margin-left: 1.4rem;
+ display: block;
+ cursor: pointer;
+ text-align: left;
+ color: @component-name-color;
+
+ &:hover {
+ background-color: @select-color;
+ }
+ }
+ }
+}
diff --git a/packages/inula-dev-tools/src/components/ComponentInfo.tsx b/packages/inula-dev-tools/src/components/ComponentInfo.tsx
new file mode 100644
index 00000000..0f2b658e
--- /dev/null
+++ b/packages/inula-dev-tools/src/components/ComponentInfo.tsx
@@ -0,0 +1,435 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+import styles from './ComponentInfo.less';
+import Eye from '../svgs/Eye';
+import Debug from '../svgs/Debug';
+import Location from '../svgs/Location';
+import Triangle from '../svgs/Triangle';
+import { memo, useContext, useEffect, useState, useRef, useMemo, createRef } from 'openinula';
+import { IData } from './VTree';
+import { buildAttrModifyData, IAttr } from '../parser/parseAttr';
+import { postMessageToBackground } from '../panelConnection';
+import { CopyToConsole, InspectDom, LogComponentData, ModifyAttrs, StorageValue } from '../utils/constants';
+import type { Source } from '../../../inula/src/renderer/Types';
+import ViewSourceContext from '../utils/ViewSource';
+import PickElementContext from '../utils/PickElement';
+import Operation from '../svgs/Operation';
+
+type IComponentInfo = {
+ name: string;
+ attrs: {
+ parsedProps?: IAttr[];
+ parsedState?: IAttr[];
+ parsedHooks?: IAttr[];
+ };
+ parents: IData[];
+ id: number;
+ source?: Source;
+ onClickParent: (item: IData) => void;
+};
+
+const ComponentAttr = memo(function ComponentAttr({
+ attrsName,
+ attrsType,
+ attrs,
+ id,
+ dropdownRef,
+ }: {
+ attrsName: string;
+ attrsType: string;
+ attrs: IAttr[];
+ id: number;
+ dropdownRef: null | HTMLElement;
+}) {
+ const [editableAttrs, setEditableAttrs] = useState(attrs);
+ const [expandNodes, setExpandNodes] = useState([]);
+
+ useEffect(() => {
+ setEditableAttrs(attrs);
+ }, [attrs]);
+
+ const handleCollapse = (item: IAttr) => {
+ const nodes = [...expandNodes];
+ const expandItem = `${item.name}_${editableAttrs.indexOf(item)}`;
+ const i = nodes.indexOf(expandItem);
+ if (i === -1) {
+ nodes.push(expandItem);
+ } else {
+ nodes.splice(i, 1);
+ }
+ setExpandNodes(nodes);
+ };
+
+ // props 展示的 key: value 中的 value 值
+ const getShowName = item => {
+ let retStr;
+ if (item === undefined) {
+ retStr = String(item);
+ } else if (typeof item === 'number') {
+ retStr = item;
+ } else if (typeof item === 'string') {
+ retStr = item.endsWith('>') ? `<${item}` : item;
+ } else {
+ retStr = `"${item}"`;
+ }
+ return retStr;
+ };
+
+ /**
+ * 拿到 props 或 hooks 在 VNode 里的路径
+ *
+ * @param {Array} editableAttrs 所有 props 与 hooks 的值
+ * @param {number} index 此值在 editableAttrs 的下标位置
+ * @param {string} attrsType 此值属于 props 还是 hooks
+ * @return {Array} 值在 vNode 里的路径
+ */
+ const getPath = (editableAttrs: IAttr[], index: number, attrsType: string): Array => {
+ const path: Array = [];
+ let local = editableAttrs[index].indentation;
+ if (local === 1) {
+ path.push(attrsType === 'Hooks' ? editableAttrs[index].hIndex : editableAttrs[index].name);
+ } else {
+ let location = local;
+ let id = index;
+ while (location > 0) {
+ // local === 1 时处于 vNode.hooks 的子元素最外层
+ if (location < local || id === index || local === 1) {
+ if (local === 1) {
+ attrsType === 'Hooks'
+ ? path.unshift(editableAttrs[id + 1].hIndex, 'state')
+ : path.unshift(editableAttrs[id + 1].name);
+ break;
+ } else {
+ if (editableAttrs[id]?.indentation === 1) {
+ if (editableAttrs[id]?.name === 'State') {
+ path.unshift('stateValue');
+ }
+ if (editableAttrs[id]?.name === 'Ref') {
+ path.unshift('current');
+ }
+ } else {
+ path.unshift(editableAttrs[id].name);
+ }
+ }
+ // 跳过同级
+ local = location;
+ }
+ location = id >= 1 ? editableAttrs[id - 1].indentation : -1;
+ id = -1;
+ }
+ }
+ return path;
+ };
+
+ const showAttr = [];
+ let currentIndentation = null;
+
+ // 为每一行数据添加一个 ref
+ const refsById = useMemo(() => {
+ const refs = {};
+ editableAttrs.forEach((item, index) => {
+ refs[index] = createRef();
+ });
+ return refs;
+ }, [editableAttrs]);
+
+ editableAttrs.forEach((item, index) => {
+ const operationRef = refsById[index];
+ const indentation = item.indentation;
+ if (currentIndentation !== null) {
+ if (indentation > currentIndentation) {
+ return;
+ } else {
+ currentIndentation = null;
+ }
+ }
+ const nextItem = editableAttrs[index + 1];
+ const hasChild = nextItem ? nextItem.indentation - item.indentation > 0 : false;
+ const isCollapsed = !expandNodes.includes(`${item.name}_${index}`);
+
+ // 按钮点击事件
+ const operationClick = (e: Event, operationRef: any) => {
+ // 防止点击按钮触发展开或者合起数据
+ e.stopPropagation();
+ if (operationRef.current) {
+ const operationRect = operationRef.current.getBoundingClientRect();
+ // 19.2 为图标按钮高度,85 为弹框高度的一半
+ dropdownRef.style.setProperty('--content-top', `${operationRect.top + 19.2}px`);
+ dropdownRef.style.setProperty('--content-left', `${operationRect.left - 85}px`);
+ }
+ dropdownRef.classList.toggle(styles['active']);
+ const attrInfo = {
+ id: { id },
+ itemName: item.name,
+ attrsName: attrsName,
+ path: getPath(editableAttrs, index, attrsName),
+ };
+ (dropdownRef as any).attrInfo = attrInfo;
+ console.log(dropdownRef);
+ };
+
+ showAttr.push(
+
+ copyToConsole(
+ (dropdownRef.current as any).attrInfo.itemName,
+ (dropdownRef.current as any).attrInfo.attrsName,
+ (dropdownRef.current as any).attrInfo.path
+ )
+ }
+ >
+ Copy value to console
+
+
storeVariable((dropdownRef.current as any).attrInfo.attrsName, (dropdownRef.current as any).attrInfo.path)}
+ >
+ Store as global variable
+
+
+
+
+
+ );
+}
+
+export default memo(ComponentInfo);
diff --git a/packages/inula-dev-tools/src/components/Search.less b/packages/inula-dev-tools/src/components/Search.less
new file mode 100644
index 00000000..72752465
--- /dev/null
+++ b/packages/inula-dev-tools/src/components/Search.less
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+.search {
+ width: 100%;
+}
diff --git a/packages/inula-dev-tools/src/components/Search.tsx b/packages/inula-dev-tools/src/components/Search.tsx
new file mode 100644
index 00000000..ea1b48c8
--- /dev/null
+++ b/packages/inula-dev-tools/src/components/Search.tsx
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+import styles from './Search.less';
+
+interface SearchProps {
+ onKeyUp: () => void;
+ onChange: (event: any) => void;
+ value: string;
+}
+
+export default function Search(props: SearchProps) {
+ const { onChange, value, onKeyUp } = props;
+ const handleChange = event => {
+ onChange(event.target.value);
+ };
+ const handleKeyUp = event => {
+ if (event.key === 'Enter') {
+ onKeyUp();
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/packages/inula-dev-tools/src/components/SizeObserver.tsx b/packages/inula-dev-tools/src/components/SizeObserver.tsx
new file mode 100644
index 00000000..32d92058
--- /dev/null
+++ b/packages/inula-dev-tools/src/components/SizeObserver.tsx
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+import { useEffect, useState, useRef } from 'openinula';
+import { addResizeListener, removeResizeListener } from './resizeEvent';
+
+export function SizeObserver(props) {
+ const { children, ...rest } = props;
+ const containerRef = useRef();
+ const [size, setSize] = useState<{ width: number; height: number }>();
+ const notifyChild = (element) => {
+ setSize({
+ width: element.offsetWidth,
+ height: element.offsetHeight,
+ });
+ };
+
+ useEffect(() => {
+ const element = containerRef.current!;
+ setSize({
+ width: element.offsetWidth,
+ height: element.offsetHeight,
+ });
+ addResizeListener(element, notifyChild);
+ return () => {
+ removeResizeListener(element, notifyChild);
+ };
+ }, []);
+ const myChild = size ? children(size.width, size.height) : null;
+
+ return (
+
+ {myChild}
+
+ );
+}
diff --git a/packages/inula-dev-tools/src/components/VList/ItemMap.ts b/packages/inula-dev-tools/src/components/VList/ItemMap.ts
new file mode 100644
index 00000000..e4c43bc3
--- /dev/null
+++ b/packages/inula-dev-tools/src/components/VList/ItemMap.ts
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+/**
+ * 用于在滚动的过程中,对比上一次渲染的结果和本次需要渲染项
+ * 确保继续渲染项在新渲染数组中的位置和旧渲染数组中的位置不发生改变
+ */
+export default class ItemMap {
+
+ // 不要用 indexOf 进行位置计算,它会遍历数组
+ private lastRenderItemToIndexMap: Map;
+
+ constructor() {
+ this.lastRenderItemToIndexMap = new Map();
+ }
+
+ public calculateReSortedItems(nextItems: T[]): (T|undefined)[] {
+ if (this.lastRenderItemToIndexMap.size === 0) {
+ nextItems.forEach((item, index) => {
+ this.lastRenderItemToIndexMap.set(item, index);
+ });
+ return nextItems;
+ }
+
+ const nextRenderItems: (T | undefined)[] = [];
+ const length = nextItems.length;
+ const nextRenderItemToIndexMap = new Map();
+ 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;
+ }
+}
diff --git a/packages/inula-dev-tools/src/components/VList/VList.less b/packages/inula-dev-tools/src/components/VList/VList.less
new file mode 100644
index 00000000..27d47999
--- /dev/null
+++ b/packages/inula-dev-tools/src/components/VList/VList.less
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+.container {
+ position: relative;
+ overflow-y: auto;
+ height: 100%;
+ width: 100%;
+}
+
+.item {
+ position: absolute;
+ width: 100%;
+}
diff --git a/packages/inula-dev-tools/src/components/VList/VList.tsx b/packages/inula-dev-tools/src/components/VList/VList.tsx
new file mode 100644
index 00000000..9efbbf89
--- /dev/null
+++ b/packages/inula-dev-tools/src/components/VList/VList.tsx
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+/**
+ * 内部只记录滚动位置状态值
+ * data 数组更新后不修改滚动位置,只有修改 scrollToItem 才会修改滚动位置
+ */
+import { useState, useRef, useEffect, useMemo } from 'openinula';
+import styles from './VList.less';
+import ItemMap from './ItemMap';
+import { debounceFunc } from '../../utils/publicUtil';
+
+interface IProps {
+ data: T[];
+ maxDeep: number;
+ width: number; // 暂时未用到,当需要支持横向滚动时使用
+ height: number; // VList 的高度
+ children?: any; // inula 组件
+ itemHeight: number;
+ scrollToItem?: T; // 滚动到指定项位置,如果该项在可见区域内,不滚动,如果补在,则滚动到中间位置
+ onRendered: (renderInfo: RenderInfoType) => void;
+ filter?: (data: T) => boolean; // false 表示该行不显示
+}
+
+export type RenderInfoType = {
+ visibleItems: T[];
+}
+
+function parseTranslate(data: T[], itemHeight: number) {
+ const map = new Map();
+ data.forEach((item, index) => {
+ map.set(item, index * itemHeight);
+ })
+ return map;
+}
+
+export function VList(props: IProps) {
+ const { data, maxDeep, height, width, children, itemHeight, scrollToItem, onRendered } = props;
+ const [scrollTop, setScrollTop] = useState(Math.max(data.indexOf(scrollToItem!), 0) * itemHeight);
+ const renderInfoRef: { current: RenderInfoType } = useRef({
+ visibleItems: [],
+ });
+ const [indentationLength, setIndentationLength] = useState(0);
+
+ // 每个 item 的 translateY 值固定不变
+ const itemToTranslateYMap = useMemo(() => parseTranslate(data, itemHeight), [data]);
+ const itemIndexMap = useMemo(() => new ItemMap(), []);
+ const containerRef = useRef();
+
+ useEffect(() => {
+ onRendered(renderInfoRef.current);
+ });
+
+ useEffect(() => {
+ debounceFunc(() => setIndentationLength(Math.min(12, Math.round(width / (2 * maxDeep)))));
+ }, [width]);
+
+ useEffect(() => {
+ if (scrollToItem) {
+ const renderInfo = renderInfoRef.current;
+ // 在显示区域,不滚动
+ if (!renderInfo.visibleItems.includes(scrollToItem)) {
+ const index = data.indexOf(scrollToItem);
+ // 显示在页面中间
+ const top = Math.max(index * itemHeight - height / 2, 0);
+ containerRef.current?.scrollTo({ top: top });
+ }
+ }
+ }, [scrollToItem]);
+
+ // 滚动事件会频繁触发,通过框架提供的代理会有大量计算寻找 dom 元素,直接绑定到原生事件上减少计算量
+ useEffect(() => {
+ const handleScroll = event => {
+ const scrollTop = event.target.scrollTop;
+ setScrollTop(scrollTop);
+ };
+ const container = containerRef.current;
+ container?.addEventListener('scroll', handleScroll);
+ return () => {
+ container?.removeEventListener('scroll', handleScroll);
+ };
+ }, []);
+
+ const totalHeight = itemHeight * data.length;
+ const maxIndex = data.length; // slice 截取渲染 item 数组时最大位置不能超过自然长度
+ // 第一个可见 item index
+ const firstInViewItemIndex = Math.floor(scrollTop / itemHeight);
+ // 可见区域前最多冗余 4 个 item
+ const startRenderIndex = Math.max(firstInViewItemIndex - 4, 0); // index 不能小于 0
+ // 最多可见数量
+ const maxInViewCount = Math.floor(height / itemHeight);
+ // 最后可见 item index
+ const lastInViewIndex = Math.min(firstInViewItemIndex + maxInViewCount, maxIndex);
+ // 记录可见 items
+ renderInfoRef.current.visibleItems = data.slice(firstInViewItemIndex, lastInViewIndex);
+ // 可见区域后冗余 4 个 item
+ const lastRenderIndex = Math.min(lastInViewIndex + 4, maxIndex);
+ // 需要渲染的 item
+ const renderItems = data.slice(startRenderIndex, lastRenderIndex);
+ // 给 items 重新排序,确保未移出渲染数组的 item 在新的渲染数组中位置不变,这样在 diff 算法比较后,这部分的 dom 不会发生更新
+ const nextRenderList = itemIndexMap.calculateReSortedItems(renderItems);
+ const list = nextRenderList.map((item, index) => {
+ if (!item) {
+ return null;
+ }
+ return (
+
+ {children(item,indentationLength)}
+
+ );
+ });
+
+ return (
+
+ {list}
+
+
+ );
+}
diff --git a/packages/inula-dev-tools/src/components/VList/index.ts b/packages/inula-dev-tools/src/components/VList/index.ts
new file mode 100644
index 00000000..944e8eb8
--- /dev/null
+++ b/packages/inula-dev-tools/src/components/VList/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+export { VList } from './VList';
+export type { RenderInfoType } from './VList';
diff --git a/packages/inula-dev-tools/src/components/VTree.less b/packages/inula-dev-tools/src/components/VTree.less
new file mode 100644
index 00000000..54d23bb7
--- /dev/null
+++ b/packages/inula-dev-tools/src/components/VTree.less
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+@import 'assets.less';
+
+.treeContainer {
+ height: 100%;
+
+ .treeItem {
+ width: 100%;
+ position: absolute;
+ line-height: 1.125rem;
+ align-items: center;
+ display: inline-flex;
+ &:hover {
+ background-color: @select-color;
+ }
+
+ .treeIcon {
+ color: @arrow-color;
+ display: inline-block;
+ width: 12px;
+ padding-left: 0.5rem;
+ }
+
+ .componentName {
+ white-space: nowrap;
+ color: @component-name-color;
+ display: inline-flex;
+ }
+
+ .componentKeyName {
+ color: @component-key-color;
+ }
+
+ .componentKeyValue {
+ color: @component-key-value-color;
+ max-width: 100px;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ display: inline-flex;
+ white-space: nowrap;
+ }
+ }
+
+ .selectedItemChild {
+ background-color: @select-item-child-color;
+ }
+
+ .select {
+ background-color: @select-color;
+ }
+}
+
+.Badge {
+ display: inline-block;
+ background-color: rgba(0, 0, 0, 0.1);
+ color: #000000;
+ padding: 0 0.25rem;
+ line-height: normal;
+ border-radius: 0.125rem;
+ margin-left: 0.25rem;
+ font-family: @attr-name-font-family;
+ font-size: 9px;
+ height: 1rem;
+}
diff --git a/packages/inula-dev-tools/src/components/VTree.tsx b/packages/inula-dev-tools/src/components/VTree.tsx
new file mode 100644
index 00000000..77918715
--- /dev/null
+++ b/packages/inula-dev-tools/src/components/VTree.tsx
@@ -0,0 +1,329 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+import { useState, useEffect, useCallback, memo } from 'openinula';
+import styles from './VTree.less';
+import Triangle from '../svgs/Triangle';
+import { createRegExp } from '../utils/regExpUtil';
+import { SizeObserver } from './SizeObserver';
+import { RenderInfoType, VList } from './VList';
+import { postMessageToBackground } from '../panelConnection';
+import { Highlight, RemoveHighlight } from '../utils/constants';
+import { NameObj } from '../parser/parseVNode';
+
+export interface IData {
+ id: number;
+ name: NameObj;
+ indentation: number;
+ userKey: string;
+}
+
+interface IItem {
+ indentationLength: number;
+ hasChild: boolean;
+ onCollapse: (data: IData) => void;
+ onClick: (id: IData) => void;
+ onMouseEnter: (id: IData) => void;
+ onMouseLeave: (id: IData) => void;
+ isCollapsed: boolean;
+ isSelect: boolean;
+ highlightValue: string;
+ data: IData;
+ isSelectedItemChild: boolean;
+}
+
+function Item(props: IItem) {
+ const {
+ hasChild,
+ onCollapse,
+ isCollapsed,
+ data,
+ onClick,
+ indentationLength,
+ onMouseEnter,
+ onMouseLeave,
+ isSelect,
+ highlightValue = '',
+ isSelectedItemChild,
+ } = props;
+
+ const { name, userKey, indentation } = data;
+
+ const isShowKey = userKey !== '';
+ const showIcon = hasChild ? : '';
+ const handleClickCollapse = () => {
+ onCollapse(data);
+ };
+ const handleClick = () => {
+ onClick(data);
+ };
+ const handleMouseEnter = () => {
+ onMouseEnter(data);
+ };
+ const handleMouseLeave = () => {
+ onMouseLeave(data);
+ };
+
+ const itemAttr: Record = {
+ className: isSelectedItemChild ? styles.selectedItemChild : styles.treeItem,
+ onClick: handleClick,
+ onMouseEnter: handleMouseEnter,
+ onMouseLeave: handleMouseLeave,
+ };
+
+ if (isSelect) {
+ itemAttr.tabIndex = 0;
+ itemAttr.className = styles.treeItem + ' ' + styles.select;
+ }
+
+ if (isSelectedItemChild) {
+ itemAttr.className = styles.treeItem + ' ' + styles.selectedItemChild;
+ }
+
+ const pushBadge = (showName: Array, badgeName: string) => {
+ showName.push(' ');
+ showName.push(