neo-cmp-cli 1.6.0-beta.8 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -14
- package/package.json +1 -1
- package/src/module/index.js +3 -3
- package/src/neo/neoService.js +13 -5
- package/src/template/antd-custom-cmp-template/package.json +1 -1
- package/src/template/echarts-custom-cmp-template/package.json +1 -1
- package/src/template/empty-custom-cmp-template/package.json +1 -1
- package/src/template/react-custom-cmp-template/package.json +1 -1
- package/src/template/react-ts-custom-cmp-template/package.json +1 -1
- package/src/template/vue2-custom-cmp-template/package.json +1 -1
- package/src/utils/cmpUtils/createCmpByZip.js +148 -25
- package/src/utils/cmpUtils/pullCmp.js +13 -5
- package/src/utils/cmpUtils/pushCmp.js +7 -1
- package/src/utils/common.js +7 -7
- package/test/demo2.js +3 -0
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@ neo-cmp-cli 是 Neo 自定义组件开发工具,基于 [AKFun](https://github.
|
|
|
12
12
|
|
|
13
13
|
### 内置的自定义组件模板
|
|
14
14
|
创建自定义组件时(执行初始化命令 neo init)可选用。
|
|
15
|
-
- **React 模板**: [react-custom-cmp-template](https://github.com/wibetter/react-custom-cmp-template)
|
|
16
15
|
- **React + TS 模板**: [react-ts-custom-cmp-template](https://github.com/wibetter/react-ts-custom-cmp-template) 内置信息卡片列表展示组件示例
|
|
16
|
+
- **React 模板(非 TS)**: [react-custom-cmp-template](https://github.com/wibetter/react-custom-cmp-template)
|
|
17
17
|
- **Antd 模板**: [antd-custom-cmp-template](https://github.com/wibetter/antd-custom-cmp-template) 内置 Antd UI 组件实现的数据仪表板展示组件示例
|
|
18
18
|
- **Neo 模板**: [neo-custom-cmp-template](https://github.com/wibetter/neo-custom-cmp-template) 支持使用平台实体数据源(内置平台实体数据对象的增删改查示例组件)
|
|
19
19
|
- **Echarts 模板**: [echarts-custom-cmp-template](https://github.com/wibetter/echarts-custom-cmp-template) Echarts 图表自定义组件(含 高德地图展示组件示例)
|
|
@@ -63,10 +63,10 @@ neo linkDebug
|
|
|
63
63
|
需在NeoCRM / 页面设计器端启动 debug 模式,并添加控制台输出的外链脚本地址,方可在页面设计器端使用当前自定义组件。
|
|
64
64
|
|
|
65
65
|
#### 7) 构建并发布到 NeoCRM
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
#####
|
|
66
|
+
执行 `neo pushCmp` 即可构建并发布自定义组件至 NeoCRM 平台。
|
|
67
|
+
**发布前请确保**:`package.json` 的 `name` 唯一、`version` 不重复,并已配置 NeoCRM 平台授权信息。
|
|
68
|
+
|
|
69
|
+
##### 需自行添加授权配置
|
|
70
70
|
```javascript
|
|
71
71
|
module.exports = {
|
|
72
72
|
neoConfig: {
|
|
@@ -86,9 +86,7 @@ module.exports = {
|
|
|
86
86
|
},
|
|
87
87
|
}
|
|
88
88
|
```
|
|
89
|
-
|
|
90
|
-
2、如何获取 安全令牌 见:[https://doc.xiaoshouyi.com](https://doc.xiaoshouyi.com) / OAuth安全认证 / 密码模式 / 获取令牌;
|
|
91
|
-
3、发布成功后即可在 NeoCRM 对应租户环境的页面设计器和表单设计器中使用此自定义组件。
|
|
89
|
+
**详细使用说明**:请参考下方「[发布自定义组件至 NeoCRM](#6发布自定义组件至-neocrm)」章节。
|
|
92
90
|
|
|
93
91
|
## 常用命令说明
|
|
94
92
|
- **neo init**: 交互式创建自定义组件(支持 -t、--name);
|
|
@@ -97,7 +95,8 @@ module.exports = {
|
|
|
97
95
|
- **neo preview**: 本地预览自定义组件内容(支持 --name),默认支持热更新与接口代理;
|
|
98
96
|
- **neo linkDebug**: 外链调试模式,在平台端页面设计器中调试自定义组件;
|
|
99
97
|
- **neo publish2oss**: 构建并上传到对象存储(支持 --name,可自定义配置对象存储);
|
|
100
|
-
- **neo pushCmp**: 构建并发布到NeoCRM平台(支持 --name
|
|
98
|
+
- **neo pushCmp**: 构建并发布到NeoCRM平台(支持 --name,需自行添加授权配置);
|
|
99
|
+
- **neo pullCmp**: 拉取线上自定义组件(NeoCRM平台)至当前项目(支持 --name,需自行添加授权配置)。
|
|
101
100
|
|
|
102
101
|
## 开发须知
|
|
103
102
|
#### 1)默认自动识别自定义组件
|
|
@@ -153,11 +152,26 @@ neo linkDebug
|
|
|
153
152
|
#### 6)发布自定义组件至 NeoCRM
|
|
154
153
|
执行 `neo pushCmp` 即可构建并发布自定义组件至 NeoCRM 平台,其构建后资源也会上传到 NeoCRM 平台端提供的 CDN 中。
|
|
155
154
|
|
|
155
|
+
##### 使用方式
|
|
156
|
+
```bash
|
|
157
|
+
# 方式一:交互式选择要发布的自定义组件
|
|
158
|
+
neo pushCmp
|
|
159
|
+
|
|
160
|
+
# 方式二:直接指定要发布的自定义组件名称
|
|
161
|
+
neo pushCmp --name=xxCmp
|
|
162
|
+
# 或
|
|
163
|
+
neo pushCmp -n xxCmp
|
|
164
|
+
```
|
|
165
|
+
|
|
156
166
|
##### 发布前请确保
|
|
157
|
-
- **package.json 的 name
|
|
158
|
-
- **version
|
|
167
|
+
- **package.json 的 name 唯一**:确保当前项目的 `package.json` 中的 `name` 字段在 NeoCRM 平台中唯一
|
|
168
|
+
- **version 不重复**:每次发布时,请更新 `package.json` 中的 `version` 字段,确保版本号不重复
|
|
169
|
+
- **已配置 NeoCRM 平台授权信息**:在 `neo.config.js` 中配置 `neoConfig.auth`
|
|
170
|
+
- **自定义组件已正确构建**:确保组件代码可以正常构建,且 `dist` 目录下有对应的构建产物
|
|
171
|
+
- **组件模型文件存在**:确保组件目录下有 `model.ts` 或 `model.js` 文件,且正确导出了模型类
|
|
159
172
|
|
|
160
173
|
##### 需自行添加授权配置
|
|
174
|
+
在项目根目录的 `neo.config.js` 文件中添加 NeoCRM 平台授权配置:
|
|
161
175
|
```javascript
|
|
162
176
|
module.exports = {
|
|
163
177
|
neoConfig: {
|
|
@@ -181,13 +195,66 @@ module.exports = {
|
|
|
181
195
|
}
|
|
182
196
|
```
|
|
183
197
|
|
|
184
|
-
#####
|
|
185
|
-
|
|
198
|
+
##### 授权配置获取方式
|
|
199
|
+
1. **客户端 ID 和客户端秘钥**:需通过创建连接器获取
|
|
200
|
+
- 访问 [销售易文档中心](https://doc.xiaoshouyi.com) / 创建连接器
|
|
201
|
+
- 创建连接器后,从客户端信息中获取 `Client_Id` 和 `Client_Secret`
|
|
202
|
+
2. **安全令牌**:如何获取安全令牌
|
|
203
|
+
- 访问 [销售易文档中心](https://doc.xiaoshouyi.com) / OAuth安全认证 / 密码模式 / 获取令牌
|
|
204
|
+
- 按照文档说明获取 8 位安全令牌
|
|
205
|
+
- `password` 字段 = 用户账户密码 + 8 位安全令牌(直接拼接,无空格或分隔符)
|
|
206
|
+
|
|
207
|
+
##### 注意事项
|
|
208
|
+
- **版本管理**:每次发布前务必更新 `package.json` 中的 `version` 字段,避免版本冲突
|
|
209
|
+
- **组件名称唯一性**:确保 `package.json` 中的 `name` 字段在平台中唯一
|
|
210
|
+
- **模型文件要求**:组件必须包含有效的模型文件(`model.ts` 或 `model.js`),且正确导出模型类
|
|
211
|
+
- **技术栈一致性**:确保组件使用的技术栈与项目配置一致(React、React+TS、Vue2 等)
|
|
212
|
+
- **构建产物完整性**:确保 `dist` 目录下包含组件的 JS 文件和模型文件(`xxCmp.js` 和 `xxCmpModel.js`)
|
|
213
|
+
- **网络连接**:发布过程需要网络连接,确保可以访问 NeoCRM 平台和 OSS 服务
|
|
214
|
+
|
|
215
|
+
##### 常见问题
|
|
216
|
+
|
|
217
|
+
**Q: 发布失败,提示"未找到 NeoCRM 平台授权配置"**
|
|
218
|
+
A: 请检查 `neo.config.js` 文件中是否配置了 `neoConfig.auth`,确保所有授权字段都已正确填写。
|
|
219
|
+
|
|
220
|
+
**Q: 发布失败,提示"未找到自定义组件模型文件"**
|
|
221
|
+
A: 请确保组件目录下有 `model.ts` 或 `model.js` 文件,且构建后在 `dist` 目录下生成了对应的 `xxCmpModel.js` 文件。
|
|
222
|
+
|
|
223
|
+
**Q: 发布失败,提示"version 不重复"**
|
|
224
|
+
A: 请更新 `package.json` 中的 `version` 字段,使用新的版本号(如从 `1.0.0` 更新为 `1.0.1`)。
|
|
225
|
+
|
|
226
|
+
**Q: 发布成功但平台端看不到组件**
|
|
227
|
+
A: 请检查组件模型文件中的 `exposedToDesigner` 属性是否为 `true`,确保组件已暴露给设计器使用。
|
|
186
228
|
|
|
187
229
|
##### 线上 NeoCRM 端使用
|
|
188
230
|
发布成功后,即可在对应租户环境下的页面设计器和表单设计器中使用此自定义组件。
|
|
189
231
|
|
|
190
|
-
#### 7
|
|
232
|
+
#### 7)拉取线上自定义组件至本地项目
|
|
233
|
+
执行 `neo pullCmp` 即可从 NeoCRM 平台拉取自定义组件源码到当前项目。
|
|
234
|
+
该命令会将线上自定义组件的源码下载并解析到当前项目的 `src/components` 目录下。
|
|
235
|
+
|
|
236
|
+
##### 使用方式
|
|
237
|
+
```bash
|
|
238
|
+
# 方式一:交互式选择要拉取的自定义组件
|
|
239
|
+
neo pullCmp
|
|
240
|
+
|
|
241
|
+
# 方式二:直接指定要拉取的自定义组件名称
|
|
242
|
+
neo pullCmp --name=xxCmp
|
|
243
|
+
# 或
|
|
244
|
+
neo pullCmp -n xxCmp
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
##### 拉取前请确保
|
|
248
|
+
- **已配置 NeoCRM 平台授权信息**(neo.config.js 中的 neoConfig.auth)
|
|
249
|
+
- **当前项目目录中不存在同名自定义组件**(`./src/components` 目录下)
|
|
250
|
+
- **拉取的组件与当前项目技术栈一致**(React、React+TS、Vue2 等)
|
|
251
|
+
|
|
252
|
+
##### 注意事项
|
|
253
|
+
- 拉取的自定义组件会覆盖本地同名组件,请谨慎操作
|
|
254
|
+
- 如果组件源码中包含新增的依赖包,拉取完成后会提示执行 `npm install` 或 `yarn install`
|
|
255
|
+
- zip 源文件会保留在 `.neo-cli/zip-source` 目录下,可手动删除
|
|
256
|
+
|
|
257
|
+
#### 8)发布自定义组件至CDN
|
|
191
258
|
执行 `neo publish2oss` 即可构建对应自定义组件,并自动将构建后资源上传到对象存储(OSS)中。
|
|
192
259
|
备注:请优先使用 neo pushCmp。
|
|
193
260
|
|
package/package.json
CHANGED
package/src/module/index.js
CHANGED
|
@@ -252,15 +252,15 @@ yargs
|
|
|
252
252
|
} else {
|
|
253
253
|
// 创建 neoService 实例
|
|
254
254
|
const neoService = new NeoService(curConfig.neoConfig);
|
|
255
|
-
const spinner = ora('
|
|
255
|
+
const spinner = ora('正在拉取线上自定义组件列表...').start();
|
|
256
256
|
const cmpList = await neoService.getCustomCmpList();
|
|
257
257
|
if (cmpList.length === 0) {
|
|
258
258
|
console.error('当前租户暂无任何自定义组件。');
|
|
259
259
|
process.exit(1);
|
|
260
260
|
}
|
|
261
|
-
spinner.
|
|
261
|
+
spinner.stop('线上自定义组件列表拉取成功。');
|
|
262
262
|
const cmpTypeChoices = cmpList.map((cmpItem) => ({
|
|
263
|
-
name: cmpItem.label
|
|
263
|
+
name: `${cmpItem.label}(${cmpItem.cmpType})`,
|
|
264
264
|
value: cmpItem.cmpType
|
|
265
265
|
}));
|
|
266
266
|
const questions = [
|
package/src/neo/neoService.js
CHANGED
|
@@ -157,9 +157,11 @@ class NeoService {
|
|
|
157
157
|
token: access_token,
|
|
158
158
|
expiresAt: Date.now() + (expiresIn - 60) * 1000
|
|
159
159
|
};
|
|
160
|
-
spinner.
|
|
160
|
+
spinner.clear();
|
|
161
|
+
spinner.stop();
|
|
161
162
|
return access_token;
|
|
162
163
|
} catch (error) {
|
|
164
|
+
spinner.fail('获取 token 失败');
|
|
163
165
|
console.error('\n获取 token 失败:', error.message);
|
|
164
166
|
console.error('\ntoken 授权地址:', tokenUrl);
|
|
165
167
|
console.error('\ntoken 请求参数:', formData);
|
|
@@ -193,9 +195,14 @@ class NeoService {
|
|
|
193
195
|
return await this.getToken();
|
|
194
196
|
} else if (this.isTokenExpired()) {
|
|
195
197
|
const spinner = ora('token 已过期,正在刷新...').start();
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
try {
|
|
199
|
+
const token = await this.refreshToken();
|
|
200
|
+
spinner.succeed('token 刷新成功。');
|
|
201
|
+
return token;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
spinner.fail('token 刷新失败。');
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
199
206
|
}
|
|
200
207
|
return this.tokenCache.token;
|
|
201
208
|
}
|
|
@@ -458,7 +465,8 @@ class NeoService {
|
|
|
458
465
|
throw new Error(`更新组件失败: ${response.data.message || '未知错误'}`);
|
|
459
466
|
}
|
|
460
467
|
|
|
461
|
-
spinner.
|
|
468
|
+
spinner.clear();
|
|
469
|
+
spinner.stop();
|
|
462
470
|
} catch (error) {
|
|
463
471
|
spinner.fail('更新组件失败。');
|
|
464
472
|
if (error.message) {
|
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const axios = require('axios');
|
|
4
4
|
const ora = require('ora');
|
|
5
5
|
const AdmZip = require('adm-zip');
|
|
6
|
+
const _ = require('lodash');
|
|
6
7
|
const { consoleTag } = require('../neoParams'); // 输出标记
|
|
7
8
|
const hasCmpTypeByDir = require('./hasCmpTypeByDir');
|
|
8
9
|
const hasNeoProject = require('../projectUtils/hasNeoProject');
|
|
@@ -14,7 +15,7 @@ const hasNeoProject = require('../projectUtils/hasNeoProject');
|
|
|
14
15
|
* @param {*} componentBaseDir 自定义组件目录
|
|
15
16
|
*/
|
|
16
17
|
module.exports = async function (cmpZipUrl, cmpName, componentBaseDir = './src/components') {
|
|
17
|
-
const finalCmpName = cmpName
|
|
18
|
+
const finalCmpName = cmpName;
|
|
18
19
|
|
|
19
20
|
if (!hasNeoProject()) {
|
|
20
21
|
console.error(
|
|
@@ -31,36 +32,75 @@ module.exports = async function (cmpZipUrl, cmpName, componentBaseDir = './src/c
|
|
|
31
32
|
// 创建临时目录
|
|
32
33
|
const tempDir = path.join(process.cwd(), '.neo-cli', 'zip-source');
|
|
33
34
|
|
|
35
|
+
const spinner = ora(`${consoleTag}正在下载组件源码...`).start();
|
|
36
|
+
|
|
34
37
|
// 下载源码文件并解析到 src/components 目录下
|
|
35
38
|
try {
|
|
36
39
|
await fs.ensureDir(tempDir); // 如果目录不存在,会自动创建(包括所有必要的父目录)
|
|
37
40
|
|
|
38
|
-
// 下载 zip 文件
|
|
39
|
-
const spinner = ora(`${consoleTag}正在下载组件源码...`).start();
|
|
40
41
|
const zipFilePath = path.join(tempDir, `${finalCmpName}.zip`);
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
// 检查是否存在同名 zip 包,如果存在则先删除
|
|
44
|
+
if (await fs.pathExists(zipFilePath)) {
|
|
45
|
+
await fs.remove(zipFilePath);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 下载 zip 文件
|
|
49
|
+
let response;
|
|
50
|
+
try {
|
|
51
|
+
response = await axios({
|
|
52
|
+
url: cmpZipUrl,
|
|
53
|
+
method: 'GET',
|
|
54
|
+
responseType: 'arraybuffer',
|
|
55
|
+
timeout: 60000, // 60秒超时
|
|
56
|
+
maxContentLength: Infinity,
|
|
57
|
+
maxBodyLength: Infinity
|
|
58
|
+
});
|
|
59
|
+
} catch (axiosError) {
|
|
60
|
+
spinner.fail(`${consoleTag}下载文件失败:`, axiosError.message);
|
|
61
|
+
throw axiosError;
|
|
62
|
+
}
|
|
47
63
|
|
|
48
64
|
// 保存 zip 文件到临时目录
|
|
49
|
-
|
|
65
|
+
try {
|
|
66
|
+
let buffer;
|
|
67
|
+
// 处理不同的数据类型
|
|
68
|
+
if (Buffer.isBuffer(response.data)) {
|
|
69
|
+
// 如果已经是 Buffer,直接使用
|
|
70
|
+
buffer = response.data;
|
|
71
|
+
} else if (response.data instanceof ArrayBuffer) {
|
|
72
|
+
// 如果是 ArrayBuffer,转换为 Buffer
|
|
73
|
+
buffer = Buffer.from(response.data);
|
|
74
|
+
} else if (response.data.buffer instanceof ArrayBuffer) {
|
|
75
|
+
// 如果是 TypedArray (如 Uint8Array),使用其 buffer
|
|
76
|
+
buffer = Buffer.from(response.data.buffer);
|
|
77
|
+
} else {
|
|
78
|
+
// 尝试直接转换
|
|
79
|
+
buffer = Buffer.from(response.data);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await fs.writeFile(zipFilePath, buffer);
|
|
83
|
+
} catch (writeError) {
|
|
84
|
+
spinner.fail(`${consoleTag}读取组件源码文件失败:`, writeError.message);
|
|
85
|
+
throw writeError;
|
|
86
|
+
}
|
|
50
87
|
|
|
51
88
|
// 解压 zip 文件
|
|
52
|
-
spinner.
|
|
89
|
+
spinner.info(`${consoleTag}正在解压组件源码...`);
|
|
90
|
+
|
|
53
91
|
const zip = new AdmZip(zipFilePath);
|
|
92
|
+
// 解压后的目录名称为组件名称:.neo-cli/zip-source/xxCmp
|
|
54
93
|
const extractPath = path.join(tempDir, finalCmpName);
|
|
55
|
-
zip.extractAllTo(extractPath, true);
|
|
94
|
+
zip.extractAllTo(extractPath, true); // 解压到临时目录
|
|
56
95
|
|
|
57
96
|
// 查找解压后的 src/components 目录
|
|
58
97
|
const cmpSourcePath = path.join(extractPath, componentBaseDir, finalCmpName);
|
|
59
98
|
|
|
60
99
|
if (!fs.existsSync(cmpSourcePath)) {
|
|
61
|
-
spinner.
|
|
62
|
-
|
|
63
|
-
|
|
100
|
+
spinner.fail(`${consoleTag}解压后的 zip 包中未找到 ${finalCmpName} 组件源码目录。`);
|
|
101
|
+
// 只删除解压目录,保留 zip 源文件
|
|
102
|
+
await fs.remove(extractPath);
|
|
103
|
+
return false;
|
|
64
104
|
}
|
|
65
105
|
|
|
66
106
|
// 确保目标目录存在
|
|
@@ -68,22 +108,105 @@ module.exports = async function (cmpZipUrl, cmpName, componentBaseDir = './src/c
|
|
|
68
108
|
await fs.ensureDir(targetComponentsDir);
|
|
69
109
|
|
|
70
110
|
// 复制 src/components 目录下的所有内容到目标目录
|
|
71
|
-
|
|
72
|
-
.copy(cmpSourcePath, targetComponentsDir)
|
|
73
|
-
|
|
111
|
+
try {
|
|
112
|
+
await fs.copy(cmpSourcePath, targetComponentsDir);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
spinner.fail(`${consoleTag}自定义组件模板下载失败:${err.message || err}`);
|
|
115
|
+
// 只删除解压目录,保留 zip 源文件
|
|
116
|
+
await fs.remove(extractPath);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 处理源码中其他文件
|
|
121
|
+
try {
|
|
122
|
+
// 标准化 componentBaseDir,去掉开头的 ./ 并统一路径分隔符
|
|
123
|
+
const normalizedComponentBaseDir = componentBaseDir.replace(/^\.\//, '').replace(/\\/g, '/');
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 递归复制文件,排除 componentBaseDir 目录
|
|
127
|
+
* @param {string} sourceDir 源目录
|
|
128
|
+
* @param {string} targetRoot 目标根目录(项目根目录)
|
|
129
|
+
* @param {string} excludeDir 要排除的目录(相对于源目录)
|
|
130
|
+
*/
|
|
131
|
+
const copyOtherFiles = async (sourceDir, targetRoot, excludeDir) => {
|
|
132
|
+
const items = await fs.readdir(sourceDir);
|
|
133
|
+
|
|
134
|
+
for (const item of items) {
|
|
135
|
+
const sourcePath = path.join(sourceDir, item); // 当前源文件路径
|
|
136
|
+
const stat = await fs.stat(sourcePath); // 当前源文件状态
|
|
137
|
+
const fileName = path.basename(sourcePath); // 当前源文件名称
|
|
138
|
+
|
|
139
|
+
// 计算相对于解压根目录的路径
|
|
140
|
+
const relativePath = path.relative(extractPath, sourcePath);
|
|
141
|
+
const normalizedRelativePath = relativePath.replace(/\\/g, '/');
|
|
142
|
+
|
|
143
|
+
// 跳过 componentBaseDir 目录及其子目录
|
|
144
|
+
if (
|
|
145
|
+
normalizedRelativePath.startsWith(excludeDir + '/') ||
|
|
146
|
+
normalizedRelativePath === excludeDir
|
|
147
|
+
) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const targetPath = path.join(targetRoot, relativePath); // 目标文件路径
|
|
152
|
+
|
|
153
|
+
if (stat.isDirectory()) {
|
|
154
|
+
// 如果是目录,递归处理
|
|
155
|
+
await copyOtherFiles(sourcePath, targetRoot, excludeDir);
|
|
156
|
+
} else if (stat.isFile()) {
|
|
157
|
+
// 如果是文件,检查目标文件是否存在
|
|
158
|
+
const targetExists = await fs.pathExists(targetPath);
|
|
159
|
+
if (!targetExists) {
|
|
160
|
+
// 确保目标目录存在
|
|
161
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
162
|
+
// 复制文件
|
|
163
|
+
await fs.copy(sourcePath, targetPath);
|
|
164
|
+
} else if (fileName === 'package.json') {
|
|
165
|
+
// 处理 package.json 文件(当目标文件已存在时)
|
|
166
|
+
const sourcePackageJson = await fs.readJson(sourcePath);
|
|
167
|
+
const targetPackageJson = await fs.readJson(targetPath);
|
|
168
|
+
|
|
169
|
+
const newPackageJsonDeps = _.omit(
|
|
170
|
+
sourcePackageJson.dependencies,
|
|
171
|
+
targetPackageJson.dependencies
|
|
172
|
+
);
|
|
173
|
+
const newDeps = Object.keys(newPackageJsonDeps);
|
|
174
|
+
if (newDeps.length > 0) {
|
|
175
|
+
console.warn(
|
|
176
|
+
`${consoleTag}检测到 package.json 中新增了 ${
|
|
177
|
+
newDeps.length
|
|
178
|
+
} 个依赖包:${newDeps.join(', ')}`
|
|
179
|
+
);
|
|
180
|
+
console.warn(
|
|
181
|
+
`${consoleTag}为确保组件正常运行,请执行以下命令安装依赖:npm install 或 yarn install`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
// 合并 package.json:将源文件的配置合并到目标文件中
|
|
185
|
+
// 注意:_.merge 会修改第一个参数,所以先克隆目标对象
|
|
186
|
+
const mergedPackageJson = _.merge({}, sourcePackageJson, targetPackageJson);
|
|
187
|
+
await fs.writeJson(targetPath, mergedPackageJson, { spaces: 2 });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
// 开始处理源码中其他文件
|
|
193
|
+
await copyOtherFiles(extractPath, process.cwd(), normalizedComponentBaseDir);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
spinner.warn(`${consoleTag}处理源码文件出现警告:${err.message || err}`);
|
|
196
|
+
}
|
|
74
197
|
|
|
75
|
-
//
|
|
76
|
-
await fs.remove(
|
|
198
|
+
// 清理解压目录,保留 zip 源文件
|
|
199
|
+
await fs.remove(extractPath);
|
|
77
200
|
spinner.succeed(`${consoleTag}已成功从 zip 包解析自定义组件(${finalCmpName})!`);
|
|
78
201
|
|
|
79
202
|
return true;
|
|
80
203
|
} catch (error) {
|
|
81
|
-
spinner.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
|
|
204
|
+
spinner.fail(`${consoleTag}从 zip 包创建自定义组件失败(${finalCmpName}):`, error);
|
|
205
|
+
// 清理解压目录,保留 zip 源文件
|
|
206
|
+
const extractPath = path.join(tempDir, finalCmpName);
|
|
207
|
+
if (await fs.pathExists(extractPath)) {
|
|
208
|
+
await fs.remove(extractPath);
|
|
209
|
+
}
|
|
87
210
|
|
|
88
211
|
return false;
|
|
89
212
|
}
|
|
@@ -16,6 +16,14 @@ const framework = getFramework(currentPackageJson.framework);
|
|
|
16
16
|
* 从 NeoCRM 拉取自定义组件
|
|
17
17
|
* @param {string} cmpType 自定义组件类型
|
|
18
18
|
* @param {object} authConfig 授权配置
|
|
19
|
+
*
|
|
20
|
+
* 拉取流程说明
|
|
21
|
+
* 1. 获取当前租户下的自定义组件列表
|
|
22
|
+
* 2. 验证组件是否存在、是否已存在本地、技术栈是否一致
|
|
23
|
+
* 3. 下载组件源码 zip 包到临时目录(`.neo-cli/zip-source`)
|
|
24
|
+
* 4. 解压并解析组件源码到 `src/components` 目录
|
|
25
|
+
* 5. 合并 `package.json` 中的依赖配置(如有新增依赖会提示安装)
|
|
26
|
+
* 6. 清理解压目录,保留 zip 源文件(便于后续问题排查)
|
|
19
27
|
*/
|
|
20
28
|
const pullCmp = async (cmpType, authConfig, _neoService) => {
|
|
21
29
|
if (!authConfig) {
|
|
@@ -52,26 +60,26 @@ const pullCmp = async (cmpType, authConfig, _neoService) => {
|
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
if (cmpList.length === 0) {
|
|
55
|
-
spinner.
|
|
63
|
+
spinner.fail('拉取失败,当前租户暂无任何自定义组件。');
|
|
56
64
|
process.exit(1);
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
// 获取自定义组件信息
|
|
60
68
|
cmpInfo = neoService.getCmpInfoByCmpType(cmpType);
|
|
61
69
|
if (!cmpInfo) {
|
|
62
|
-
spinner.
|
|
70
|
+
spinner.fail(`拉取失败,当前租户不存在${cmpType}自定义组件。`);
|
|
63
71
|
process.exit(1);
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
// 判断拉取的组件和当前项目是否为同一技术栈
|
|
67
75
|
if (cmpInfo.framework !== framework) {
|
|
68
|
-
spinner.
|
|
76
|
+
spinner.fail(`拉取失败,${cmpType}自定义组件与当前项目技术栈不一致。`);
|
|
69
77
|
process.exit(1);
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
// 获取当前源码
|
|
73
81
|
if (!cmpInfo.codeLib) {
|
|
74
|
-
spinner.
|
|
82
|
+
spinner.fail(`拉取失败,${cmpType}自定义组件未记录对应的源码文件。`);
|
|
75
83
|
process.exit(1);
|
|
76
84
|
}
|
|
77
85
|
|
|
@@ -81,7 +89,7 @@ const pullCmp = async (cmpType, authConfig, _neoService) => {
|
|
|
81
89
|
// 将zip 包里面的自定义组件源码解析到 src/components 目录下
|
|
82
90
|
const cmpResult = await createCmpByZip(codeLib, cmpType, './src/components');
|
|
83
91
|
if (!cmpResult) {
|
|
84
|
-
spinner.
|
|
92
|
+
spinner.fail(`拉取失败,${cmpType}自定义组件源码解析失败,请检查源码文件是否正确。`);
|
|
85
93
|
process.exit(1);
|
|
86
94
|
}
|
|
87
95
|
|
|
@@ -82,7 +82,7 @@ const buildComponentData = async (assetsRoot, cmpInfo) => {
|
|
|
82
82
|
plugin: cmpInfo.modelAsset,
|
|
83
83
|
version: currentPackageJson.version || '1.0.0',
|
|
84
84
|
// 技术栈标识: 从 package.json / framework 字段获取,没有则默认 为 react ts 技术栈
|
|
85
|
-
framework: currentPackageJson.framework ? getFramework(currentPackageJson.framework) :
|
|
85
|
+
framework: currentPackageJson.framework ? getFramework(currentPackageJson.framework) : 0, // 0: React, 1: vue2, 2: jQuery, 3: vue3
|
|
86
86
|
// 从模型实例中提取并设置组件信息
|
|
87
87
|
label: modelInstance.label || cmpType,
|
|
88
88
|
description: modelInstance.description || '',
|
|
@@ -121,6 +121,12 @@ const buildComponentData = async (assetsRoot, cmpInfo) => {
|
|
|
121
121
|
* 发布组件到 NeoCRM
|
|
122
122
|
* @param {object} config 配置信息
|
|
123
123
|
* @param {string} cmpType 自定义组件类型
|
|
124
|
+
*
|
|
125
|
+
* 发布流程说明
|
|
126
|
+
* 1. 打包源码文件:将自定义组件源码打包成 zip 文件
|
|
127
|
+
* 2. 上传构建产物到 OSS:将构建后的资源文件上传到 NeoCRM 平台提供的 CDN
|
|
128
|
+
* 3. 构建组件数据:从组件模型文件中提取组件信息
|
|
129
|
+
* 4. 保存组件信息到 NeoCRM 平台:将组件信息保存到平台数据库
|
|
124
130
|
*/
|
|
125
131
|
const pushCmp = async (config, cmpType) => {
|
|
126
132
|
const { auth: credentials } = config;
|
package/src/utils/common.js
CHANGED
|
@@ -5,7 +5,7 @@ const { consoleTag } = require('./neoParams');
|
|
|
5
5
|
* 0: React, 1: vue2, 2: jQuery, 3: vue3
|
|
6
6
|
*/
|
|
7
7
|
function getFramework(_framework) {
|
|
8
|
-
let defaultFramework =
|
|
8
|
+
let defaultFramework = 0; // 默认 React 技术栈
|
|
9
9
|
if (!_framework) {
|
|
10
10
|
return defaultFramework;
|
|
11
11
|
}
|
|
@@ -13,32 +13,32 @@ function getFramework(_framework) {
|
|
|
13
13
|
switch (curFramework) {
|
|
14
14
|
case 'jquery':
|
|
15
15
|
case 'jq':
|
|
16
|
-
curFramework =
|
|
16
|
+
curFramework = 2;
|
|
17
17
|
break;
|
|
18
18
|
case 'vue2':
|
|
19
19
|
case 'vue 2':
|
|
20
20
|
case 'vue2.0':
|
|
21
21
|
case 'vue 2.0':
|
|
22
|
-
curFramework =
|
|
22
|
+
curFramework = 1;
|
|
23
23
|
break;
|
|
24
24
|
case 'vue':
|
|
25
25
|
case 'vue3':
|
|
26
26
|
case 'vue 3':
|
|
27
27
|
case 'vue3.0':
|
|
28
28
|
case 'vue 3.0':
|
|
29
|
-
curFramework =
|
|
29
|
+
curFramework = 3;
|
|
30
30
|
console.error(`${consoleTag} 暂不支持 vue3.0 技术栈。`);
|
|
31
31
|
break;
|
|
32
32
|
case 'react-js':
|
|
33
33
|
console.error(`${consoleTag} 暂不支持 react-js 技术栈。`);
|
|
34
|
-
curFramework =
|
|
34
|
+
curFramework = 0;
|
|
35
35
|
break;
|
|
36
36
|
case 'react':
|
|
37
37
|
case 'react-ts':
|
|
38
|
-
curFramework =
|
|
38
|
+
curFramework = 0;
|
|
39
39
|
break;
|
|
40
40
|
default:
|
|
41
|
-
curFramework =
|
|
41
|
+
curFramework = 0;
|
|
42
42
|
}
|
|
43
43
|
return curFramework;
|
|
44
44
|
}
|
package/test/demo2.js
ADDED