hcordova 1.0.0

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.
Files changed (55) hide show
  1. package/LICENSE +204 -0
  2. package/README.md +346 -0
  3. package/bin/hcordova +27 -0
  4. package/package.json +39 -0
  5. package/src/base-cli.js +1181 -0
  6. package/src/utils/DependencyChecker.js +382 -0
  7. package/src/utils/PlatformProject.js +452 -0
  8. package/src/utils/PluginConfigParser.js +408 -0
  9. package/src/utils/PluginDownloader.js +181 -0
  10. package/src/utils/PluginHandler.js +1097 -0
  11. package/src/utils/VariableValidator.js +369 -0
  12. package/src/utils/args-processor.js +79 -0
  13. package/templates/project/AppScope/app.json5 +10 -0
  14. package/templates/project/AppScope/resources/base/element/string.json +8 -0
  15. package/templates/project/AppScope/resources/base/media/background.png +0 -0
  16. package/templates/project/AppScope/resources/base/media/foreground.png +0 -0
  17. package/templates/project/AppScope/resources/base/media/layered_image.json +7 -0
  18. package/templates/project/build-profile.json5 +46 -0
  19. package/templates/project/code-linter.json5 +32 -0
  20. package/templates/project/entry/build-profile.json5 +28 -0
  21. package/templates/project/entry/hvigorfile.ts +6 -0
  22. package/templates/project/entry/obfuscation-rules.txt +23 -0
  23. package/templates/project/entry/oh-package.json5 +12 -0
  24. package/templates/project/entry/src/main/ets/entryability/EntryAbility.ets +62 -0
  25. package/templates/project/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets +32 -0
  26. package/templates/project/entry/src/main/ets/pages/Index.ets +40 -0
  27. package/templates/project/entry/src/main/module.json5 +52 -0
  28. package/templates/project/entry/src/main/resources/base/element/color.json +8 -0
  29. package/templates/project/entry/src/main/resources/base/element/float.json +8 -0
  30. package/templates/project/entry/src/main/resources/base/element/string.json +16 -0
  31. package/templates/project/entry/src/main/resources/base/media/background.png +0 -0
  32. package/templates/project/entry/src/main/resources/base/media/foreground.png +0 -0
  33. package/templates/project/entry/src/main/resources/base/media/layered_image.json +7 -0
  34. package/templates/project/entry/src/main/resources/base/media/startIcon.png +0 -0
  35. package/templates/project/entry/src/main/resources/base/profile/backup_config.json +3 -0
  36. package/templates/project/entry/src/main/resources/base/profile/main_pages.json +5 -0
  37. package/templates/project/entry/src/main/resources/dark/element/color.json +8 -0
  38. package/templates/project/entry/src/main/resources/rawfile/config.xml +26 -0
  39. package/templates/project/entry/src/main/resources/rawfile/www/cordova.js +1925 -0
  40. package/templates/project/entry/src/main/resources/rawfile/www/css/index.css +158 -0
  41. package/templates/project/entry/src/main/resources/rawfile/www/img/cordova.png +0 -0
  42. package/templates/project/entry/src/main/resources/rawfile/www/img/logo.png +0 -0
  43. package/templates/project/entry/src/main/resources/rawfile/www/index.html +110 -0
  44. package/templates/project/entry/src/main/resources/rawfile/www/js/index.js +68 -0
  45. package/templates/project/entry/src/mock/mock-config.json5 +2 -0
  46. package/templates/project/entry/src/ohosTest/ets/test/Ability.test.ets +35 -0
  47. package/templates/project/entry/src/ohosTest/ets/test/List.test.ets +5 -0
  48. package/templates/project/entry/src/ohosTest/module.json5 +13 -0
  49. package/templates/project/entry/src/test/List.test.ets +5 -0
  50. package/templates/project/entry/src/test/LocalUnit.test.ets +33 -0
  51. package/templates/project/hvigor/hvigor-config.json5 +22 -0
  52. package/templates/project/hvigorfile.ts +6 -0
  53. package/templates/project/local.properties +9 -0
  54. package/templates/project/oh-package-lock.json5 +27 -0
  55. package/templates/project/oh-package.json5 +10 -0
@@ -0,0 +1,408 @@
1
+ /*
2
+ * Copyright (c) 2025 Huawei Device, Inc. Ltd. and <马弓手>.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ class PluginConfigParser {
21
+ constructor(api) {
22
+ this.api = api;
23
+ this.events = api.events;
24
+ }
25
+
26
+ /**
27
+ * 解析插件配置
28
+ */
29
+ parse(pluginDir, pluginId, pluginPackage) {
30
+ const pluginXmlPath = path.join(pluginDir, 'plugin.xml');
31
+
32
+ if (!fs.existsSync(pluginXmlPath)) {
33
+ throw new Error(`plugin.xml not found in plugin directory: ${pluginDir}`);
34
+ }
35
+
36
+ try {
37
+ const pluginXmlContent = fs.readFileSync(pluginXmlPath, 'utf8');
38
+ return this._parsePluginXml(pluginXmlContent, pluginDir, pluginId, pluginPackage);
39
+ } catch (error) {
40
+ throw new Error(`Failed to parse plugin.xml: ${error.message}`);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 解析plugin.xml内容
46
+ */
47
+ _parsePluginXml(xmlContent, pluginDir, pluginId, pluginPackage) {
48
+ const config = {
49
+ id: this._parsePluginId(pluginId),
50
+ dir: pluginDir,
51
+ package: pluginPackage,
52
+ name: this._extractTextContent(xmlContent, 'name'),
53
+ description: this._extractTextContent(xmlContent, 'description'),
54
+ license: this._extractTextContent(xmlContent, 'license'),
55
+ keywords: this._extractKeywords(xmlContent),
56
+ repo: this._extractTextContent(xmlContent, 'repo'),
57
+ issue: this._extractTextContent(xmlContent, 'issue'),
58
+ engines: this._parseEngines(xmlContent),
59
+ preferences: this._parsePreferences(xmlContent), // 新增:解析preference变量
60
+ dependencies: this._parseDependencies(xmlContent), // 添加依赖解析
61
+ platform: {},
62
+
63
+ };
64
+
65
+ // 解析平台特定配置
66
+ const platformMatch = xmlContent.match(/<platform\s+name="ohos">([\s\S]*?)<\/platform>/);
67
+ if (platformMatch) {
68
+ config.platform = this._parseOhosPlatformConfig(platformMatch[1], pluginDir);
69
+ }
70
+
71
+ return config;
72
+ }
73
+
74
+ _parsePluginId(pluginId) {
75
+ pluginId = pluginId.trim();
76
+
77
+ // 1. Git URL - 返回仓库名(去掉.git后缀)
78
+ if (pluginId.startsWith('http') || pluginId.startsWith('git@') || pluginId.endsWith('.git')) {
79
+ return pluginId.split('/').pop().replace('.git', '');
80
+ }
81
+
82
+ // 2. 本地路径 - 返回目录名
83
+ if (pluginId.includes('\\') || pluginId.includes('/') || pluginId.startsWith('.') || path.isAbsolute(pluginId)) {
84
+ return path.basename(pluginId);
85
+ }
86
+
87
+ // 3. 直接返回插件ID
88
+ return pluginId;
89
+ }
90
+ /**
91
+ * 提取文本内容
92
+ */
93
+ _extractTextContent(xmlContent, tagName) {
94
+ const match = xmlContent.match(new RegExp(`<${tagName}>([\\s\\S]*?)<\/${tagName}>`));
95
+ return match ? match[1].trim() : null;
96
+ }
97
+
98
+ /**
99
+ * 提取关键词
100
+ */
101
+ _extractKeywords(xmlContent) {
102
+ const keywordsMatch = xmlContent.match(/<keywords>([\s\S]*?)<\/keywords>/);
103
+ if (!keywordsMatch) return [];
104
+
105
+ const keywordsText = keywordsMatch[1].trim();
106
+ return keywordsText.split(',').map(k => k.trim()).filter(k => k);
107
+ }
108
+
109
+ /**
110
+ * 解析引擎要求
111
+ */
112
+ _parseEngines(xmlContent) {
113
+ const engines = {};
114
+ const engineMatches = xmlContent.match(/<engine\s+name="([^"]+)"\s+version="([^"]+)"\s*\/>/g) || [];
115
+
116
+ engineMatches.forEach(match => {
117
+ const nameMatch = match.match(/name="([^"]+)"/);
118
+ const versionMatch = match.match(/version="([^"]+)"/);
119
+ if (nameMatch && versionMatch) {
120
+ engines[nameMatch[1]] = versionMatch[1];
121
+ }
122
+ });
123
+
124
+ return engines;
125
+ }
126
+
127
+ /**
128
+ * 解析preference变量声明
129
+ */
130
+ _parsePreferences(xmlContent) {
131
+ const preferences = {};
132
+ const preferenceMatches = xmlContent.match(/<preference\s+name="([^"]+)"\s+default="([^"]*)"(?:\s+required="([^"]*)")?\s*\/>/g) || [];
133
+
134
+ preferenceMatches.forEach(match => {
135
+ const nameMatch = match.match(/name="([^"]+)"/);
136
+ const defaultMatch = match.match(/default="([^"]*)"/);
137
+ const requiredMatch = match.match(/required="([^"]*)"/);
138
+
139
+ if (nameMatch) {
140
+ const name = nameMatch[1];
141
+ const defaultValue = defaultMatch ? defaultMatch[1] : '';
142
+ const required = requiredMatch ? requiredMatch[1].toLowerCase() === 'true' : false;
143
+
144
+ preferences[name] = {
145
+ name: name,
146
+ default: defaultValue,
147
+ required: required
148
+ };
149
+ }
150
+ });
151
+
152
+ return preferences;
153
+ }
154
+
155
+ /**
156
+ * 解析鸿蒙平台特定配置
157
+ */
158
+ _parseOhosPlatformConfig(platformContent, pluginDir) {
159
+ const config = {
160
+ configFiles: [],
161
+ cmakeConfigs: [],
162
+ sourceFiles: [],
163
+ jsModules: [],
164
+ features: [],
165
+ preferences: this._parsePlatformPreferences(platformContent) // 新增:解析平台特定的preference
166
+ };
167
+
168
+ // 解析config-file元素
169
+ const configFileMatches = platformContent.match(/<config-file\s+target="([^"]+)"\s+parent="([^"]*)"(?:\s+modules-targets-name="([^"]*)")?\s*>([\s\S]*?)<\/config-file>/g) || [];
170
+ config.configFiles = configFileMatches.map(match => {
171
+ const targetMatch = match.match(/target="([^"]+)"/);
172
+ const parentMatch = match.match(/parent="([^"]*)"/);
173
+ const modulesTargetsNameMatch = match.match(/modules-targets-name="([^"]*)"/);
174
+ const content = this._extractTagContent(match, 'config-file');
175
+
176
+ return {
177
+ target: targetMatch ? targetMatch[1] : null,
178
+ parent: parentMatch ? parentMatch[1] : '/*',
179
+ modulesTargetsName: modulesTargetsNameMatch ? modulesTargetsNameMatch[1] : 'default',
180
+ content: content,
181
+ type: 'config-file'
182
+ };
183
+ });
184
+
185
+ // 解析CMakeLists元素
186
+ const cmakeMatches = platformContent.match(/<CMakeLists\s+target="([^"]+)"\s+modules-name="([^"]*)"\s*>([\s\S]*?)<\/CMakeLists>/g) || [];
187
+ config.cmakeConfigs = cmakeMatches.map(match => {
188
+ const targetMatch = match.match(/target="([^"]+)"/);
189
+ const modulesNameMatch = match.match(/modules-name="([^"]*)"/);
190
+ const content = this._extractTagContent(match, 'CMakeLists');
191
+
192
+ // 解析CMakeLists中的param元素
193
+ const params = [];
194
+ const paramMatches = content.match(/<param\s+target="([^"]+)"\s+value="([^"]+)"\s*\/>/g) || [];
195
+ paramMatches.forEach(paramMatch => {
196
+ const targetMatch = paramMatch.match(/target="([^"]+)"/);
197
+ const valueMatch = paramMatch.match(/value="([^"]+)"/);
198
+ if (targetMatch && valueMatch) {
199
+ params.push({
200
+ target: targetMatch[1],
201
+ value: valueMatch[1]
202
+ });
203
+ }
204
+ });
205
+
206
+ return {
207
+ target: targetMatch ? targetMatch[1] : null,
208
+ modulesName: modulesNameMatch ? modulesNameMatch[1] : 'cordova',
209
+ params: params,
210
+ type: 'cmake-config'
211
+ };
212
+ });
213
+
214
+ // 解析source-file元素
215
+ const sourceFileMatches = platformContent.match(/<source-file\s+type="([^"]+)"\s+src="([^"]+)"\s+target-dir="([^"]+)"(?:\s+modules-name="([^"]*)")?(?:\s+runtimeOnly="([^"]*)")?\s*\/>/g) || [];
216
+ config.sourceFiles = sourceFileMatches.map(match => {
217
+ const typeMatch = match.match(/type="([^"]+)"/);
218
+ const srcMatch = match.match(/src="([^"]+)"/);
219
+ const targetDirMatch = match.match(/target-dir="([^"]+)"/);
220
+ const modulesNameMatch = match.match(/modules-name="([^"]*)"/);
221
+ const runtimeOnlyMatch = match.match(/runtimeOnly="([^"]*)"/);
222
+
223
+ return {
224
+ type: typeMatch ? typeMatch[1] : null, // h, cpp 等
225
+ src: srcMatch ? srcMatch[1] : null,
226
+ targetDir: targetDirMatch ? targetDirMatch[1] : null,
227
+ modulesName: modulesNameMatch ? modulesNameMatch[1] : 'cordova',
228
+ runtimeOnly: runtimeOnlyMatch ? runtimeOnlyMatch[1].toLowerCase() === 'true' : false,
229
+ fileType: 'source-file'
230
+ };
231
+ });
232
+
233
+ // 解析js-module元素
234
+ const jsModuleMatches = platformContent.match(/<js-module\s+src="([^"]+)"\s+name="([^"]+)"(?:\s+modules-targets-name="([^"]*)")?\s*>([\s\S]*?)<\/js-module>/g) || [];
235
+ config.jsModules = jsModuleMatches.map(match => {
236
+ const srcMatch = match.match(/src="([^"]+)"/);
237
+ const nameMatch = match.match(/name="([^"]+)"/);
238
+ const modulesTargetsNameMatch = match.match(/modules-targets-name="([^"]*)"/);
239
+ const content = this._extractTagContent(match, 'js-module');
240
+
241
+ // 解析clobbers元素
242
+ const clobbers = [];
243
+ const clobberMatches = content.match(/<clobbers\s+target="([^"]+)"\s*\/>/g) || [];
244
+ clobberMatches.forEach(clobberMatch => {
245
+ const targetMatch = clobberMatch.match(/target="([^"]+)"/);
246
+ if (targetMatch) {
247
+ clobbers.push({
248
+ target: targetMatch[1]
249
+ });
250
+ }
251
+ });
252
+
253
+ return {
254
+ src: srcMatch ? srcMatch[1] : null,
255
+ name: nameMatch ? nameMatch[1] : null,
256
+ modulesTargetsName: modulesTargetsNameMatch ? modulesTargetsNameMatch[1] : 'default',
257
+ clobbers: clobbers,
258
+ type: 'js-module'
259
+ };
260
+ });
261
+
262
+ // 解析config-file中的feature元素
263
+ config.configFiles.forEach(configFile => {
264
+ const featureMatches = configFile.content.match(/<feature\s+name="([^"]+)">([\s\S]*?)<\/feature>/g) || [];
265
+ featureMatches.forEach(featureMatch => {
266
+ const nameMatch = featureMatch.match(/name="([^"]+)"/);
267
+ const content = this._extractTagContent(featureMatch, 'feature');
268
+
269
+ if (nameMatch) {
270
+ const feature = {
271
+ name: nameMatch[1],
272
+ params: []
273
+ };
274
+
275
+ // 解析feature中的param元素
276
+ const paramMatches = content.match(/<param\s+name="([^"]+)"\s+value="([^"]+)"\s*\/>/g) || [];
277
+ paramMatches.forEach(paramMatch => {
278
+ const nameMatch = paramMatch.match(/name="([^"]+)"/);
279
+ const valueMatch = paramMatch.match(/value="([^"]+)"/);
280
+ if (nameMatch && valueMatch) {
281
+ feature.params.push({
282
+ name: nameMatch[1],
283
+ value: valueMatch[1]
284
+ });
285
+ }
286
+ });
287
+
288
+ config.features.push(feature);
289
+ }
290
+ });
291
+ });
292
+
293
+ return config;
294
+ }
295
+
296
+ /**
297
+ * 解析平台特定的preference变量
298
+ */
299
+ _parsePlatformPreferences(platformContent) {
300
+ const preferences = {};
301
+ const preferenceMatches = platformContent.match(/<preference\s+name="([^"]+)"\s+default="([^"]*)"(?:\s+required="([^"]*)")?\s*\/>/g) || [];
302
+
303
+ preferenceMatches.forEach(match => {
304
+ const nameMatch = match.match(/name="([^"]+)"/);
305
+ const defaultMatch = match.match(/default="([^"]*)"/);
306
+ const requiredMatch = match.match(/required="([^"]*)"/);
307
+
308
+ if (nameMatch) {
309
+ const name = nameMatch[1];
310
+ const defaultValue = defaultMatch ? defaultMatch[1] : '';
311
+ const required = requiredMatch ? requiredMatch[1].toLowerCase() === 'true' : false;
312
+
313
+ preferences[name] = {
314
+ name: name,
315
+ default: defaultValue,
316
+ required: required,
317
+ platformSpecific: true
318
+ };
319
+ }
320
+ });
321
+
322
+ return preferences;
323
+ }
324
+
325
+ /**
326
+ * 提取标签内容
327
+ */
328
+ _extractTagContent(xml, tagName) {
329
+ const regex = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)<\/${tagName}>`);
330
+ const match = xml.match(regex);
331
+ return match ? match[1].trim() : '';
332
+ }
333
+
334
+ /**
335
+ * 解析依赖项
336
+ */
337
+ _parseDependencies(xmlContent) {
338
+ const dependencies = [];
339
+ const dependencyMatches = xmlContent.match(/<dependency\s+([^>]+)\s*\/>/g) || [];
340
+
341
+ dependencyMatches.forEach(match => {
342
+ const dependency = {};
343
+
344
+ // 解析 id 属性
345
+ const idMatch = match.match(/id="([^"]+)"/);
346
+ if (idMatch) {
347
+ dependency.id = idMatch[1];
348
+ }
349
+
350
+ // 解析 url 属性
351
+ const urlMatch = match.match(/url="([^"]+)"/);
352
+ if (urlMatch) {
353
+ dependency.url = urlMatch[1];
354
+ }
355
+
356
+ // 解析 commit 属性
357
+ const commitMatch = match.match(/commit="([^"]+)"/);
358
+ if (commitMatch) {
359
+ dependency.commit = commitMatch[1];
360
+ }
361
+
362
+ // 解析 branch 属性
363
+ const branchMatch = match.match(/branch="([^"]+)"/);
364
+ if (branchMatch) {
365
+ dependency.branch = branchMatch[1];
366
+ }
367
+
368
+ // 解析 version 属性
369
+ const versionMatch = match.match(/version="([^"]+)"/);
370
+ if (versionMatch) {
371
+ dependency.version = versionMatch[1];
372
+ }
373
+
374
+ // 解析 subdir 属性
375
+ const subdirMatch = match.match(/subdir="([^"]+)"/);
376
+ if (subdirMatch) {
377
+ dependency.subdir = subdirMatch[1];
378
+ }
379
+
380
+ // 只有包含有效 id 的依赖才添加到列表
381
+ if (dependency.id) {
382
+ dependencies.push(dependency);
383
+ }
384
+ });
385
+
386
+ return dependencies;
387
+ }
388
+
389
+ /**
390
+ * 读取插件的package.json
391
+ */
392
+ readPluginPackage(pluginDir) {
393
+ const packageJsonPath = path.join(pluginDir, 'package.json');
394
+
395
+ if (!fs.existsSync(packageJsonPath)) {
396
+ throw new Error(`package.json not found in plugin directory: ${pluginDir}`);
397
+ }
398
+
399
+ try {
400
+ const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
401
+ return JSON.parse(packageJsonContent);
402
+ } catch (error) {
403
+ throw new Error(`Failed to parse package.json: ${error.message}`);
404
+ }
405
+ }
406
+ }
407
+
408
+ module.exports = PluginConfigParser;
@@ -0,0 +1,181 @@
1
+ /*
2
+ * Copyright (c) 2025 Huawei Device, Inc. Ltd. and <马弓手>.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ const path = require('path');
18
+ const fs = require('fs');
19
+ const { execSync } = require('child_process');
20
+
21
+ class PluginDownloader {
22
+ constructor(api) {
23
+ this.api = api;
24
+ this.events = api.events;
25
+ }
26
+
27
+ /**
28
+ * 下载插件
29
+ * @param {string} pluginId - 插件ID(可以是路径、git地址、npm包名)
30
+ * @param {string} targetDir - 目标目录
31
+ * @param {Object} options - 选项
32
+ * @returns {Promise<string>} 插件本地路径
33
+ */
34
+ async download(pluginId, targetDir, options = {}) {
35
+ this.events.emit('log', `Downloading plugin: ${pluginId}`);
36
+
37
+ try {
38
+ // 判断插件来源类型
39
+ const sourceType = this._getSourceType(pluginId);
40
+ switch (sourceType) {
41
+ case 'local':
42
+ return await this._handleLocalPlugin(pluginId, targetDir);
43
+ case 'git':
44
+ return await this._handleGitPlugin(pluginId, targetDir, options);
45
+ default:
46
+ throw new Error(`Unsupported plugin source: ${pluginId}`);
47
+ }
48
+ } catch (error) {
49
+ this.events.emit('error', `Failed to download plugin ${pluginId}: ${error.message}`);
50
+ throw error;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * 获取插件来源类型
56
+ */
57
+ _getSourceType(pluginId) {
58
+ if (fs.existsSync(pluginId)) {
59
+ return 'local';
60
+ } else if (pluginId.startsWith('git+') ||
61
+ pluginId.includes('.git') ||
62
+ pluginId.includes('github.com') ||
63
+ pluginId.includes('gitee.com') ||
64
+ pluginId.includes('gitcode.com')) {
65
+ return 'git';
66
+ } else {
67
+ return 'git'; // 假设HTTP/HTTPS链接也是git仓库
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 处理本地插件
73
+ */
74
+ async _handleLocalPlugin(pluginPath, targetDir) {
75
+ this.events.emit('log', `Handling local plugin: ${pluginPath}`);
76
+
77
+ const absolutePath = path.resolve(pluginPath);
78
+ if (!fs.existsSync(absolutePath)) {
79
+ throw new Error(`Local plugin path does not exist: ${absolutePath}`);
80
+ }
81
+
82
+ try{
83
+ // 如果是目录,直接返回路径
84
+ const stats = fs.statSync(absolutePath);
85
+ if (stats.isDirectory()) {
86
+ const pluginName = this._extractPluginName(pluginPath);
87
+ const targetPath = path.join(targetDir, pluginName);
88
+ this.copyDir(absolutePath, targetPath);
89
+ return targetPath;
90
+ }
91
+ } catch (error) {
92
+ this.events.emit('Error', `copy file failed: ${error.message}`);
93
+ }
94
+ // 如果是文件,可能是压缩包,需要解压
95
+ throw new Error('File-based plugins not yet supported, please use directory or git repository');
96
+ }
97
+
98
+ /**
99
+ * 处理Git插件
100
+ */
101
+ async _handleGitPlugin(gitUrl, targetDir, options) {
102
+ if(!gitUrl.startsWith('http://') && !gitUrl.startsWith('https://')) {
103
+ gitUrl = "https://gitcode.com/OpenHarmony-Cordova/"+gitUrl+".git";
104
+ }
105
+ this.events.emit('log', `Cloning git repository: ${gitUrl}`);
106
+
107
+ const pluginName = this._extractPluginName(gitUrl);
108
+ const pluginDir = path.join(targetDir, pluginName);
109
+
110
+ // 清理目标目录
111
+ if (fs.existsSync(pluginDir)) {
112
+ fs.rmSync(pluginDir, { recursive: true, force: true });
113
+ }
114
+
115
+ try {
116
+ // 克隆仓库
117
+ const gitCommand = `git clone ${gitUrl} ${pluginDir}`;
118
+ execSync(gitCommand, { stdio: 'inherit' });
119
+
120
+ // 如果指定了分支或标签,切换到指定版本
121
+ if (options.gitRef) {
122
+ execSync(`git checkout ${options.gitRef}`, {
123
+ cwd: pluginDir,
124
+ stdio: 'inherit'
125
+ });
126
+ }
127
+
128
+ this.events.emit('log', `Successfully cloned plugin to: ${pluginDir}`);
129
+ return pluginDir;
130
+ } catch (error) {
131
+ // 清理失败的文件
132
+ if (fs.existsSync(pluginDir)) {
133
+ fs.rmSync(pluginDir, { recursive: true, force: true });
134
+ }
135
+ throw new Error(`Git clone failed: ${error.message}`);
136
+ }
137
+ }
138
+
139
+ copyDir(src, dest) {
140
+ // 创建目标目录
141
+ fs.mkdirSync(dest, { recursive: true,force:true});
142
+
143
+ // 读取源目录内容
144
+ const entries = fs.readdirSync(src, { withFileTypes: true });
145
+
146
+ for (const entry of entries) {
147
+ const srcPath = path.join(src, entry.name);
148
+ const destPath = path.join(dest, entry.name);
149
+
150
+ if (entry.isDirectory()) {
151
+ // 如果是目录,递归拷贝
152
+ this.copyDir(srcPath, destPath);
153
+ } else {
154
+ // 如果是文件,直接拷贝
155
+ fs.copyFileSync(srcPath, destPath);
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * 从URL或包名中提取插件名称
162
+ */
163
+ _extractPluginName(source) {
164
+ // 处理git URL
165
+ if (source.includes('.git')) {
166
+ const match = source.match(/\/([^\/]+)\.git$/);
167
+ if (match) return match[1];
168
+ }
169
+
170
+ // 处理普通URL
171
+ if (source.includes('/')) {
172
+ const parts = source.split('/');
173
+ return parts[parts.length - 1];
174
+ }
175
+
176
+ // 处理npm包名(可能包含scope)
177
+ return source.replace(/^@/, '').replace(/\//g, '-');
178
+ }
179
+ }
180
+
181
+ module.exports = PluginDownloader;