@cloudcare/cocos-sdk 0.1.0-alpha.4 → 0.1.0-alpha.5

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 CHANGED
@@ -18,6 +18,42 @@ Guance Cloud Application Monitoring collects and analyzes RUM, Log, Trace, and S
18
18
 
19
19
  Cocos Creator 3.0–3.6.2 is supported on a best-effort basis because the stable native build extension API starts at 3.6.3. Web, mini-game, and desktop targets are not currently supported.
20
20
 
21
+ ## iOS dependency manager
22
+
23
+ CocoaPods remains the default. To use Swift Package Manager, add
24
+ `cocos-sdk.config.json` to your **Cocos project root** (beside `assets`):
25
+
26
+ ```json
27
+ {
28
+ "ios": {
29
+ "dependencyManager": "spm"
30
+ }
31
+ }
32
+ ```
33
+
34
+ Alternatively, save this setting when installing the extension:
35
+
36
+ ```sh
37
+ npx @cloudcare/cocos-sdk install --ios-dependency-manager spm
38
+ ```
39
+
40
+ Rebuild the native iOS project in Creator after changing the configuration. The
41
+ extension links the local `FTCocosBridge` package and Xcode resolves the pinned
42
+ iOS SDK from Git. Fresh SPM projects do not require `pod install`; open the
43
+ `.xcodeproj`. If the host uses CocoaPods for other libraries, continue opening
44
+ its `.xcworkspace`.
45
+
46
+ When switching an existing installation, the extension removes its managed SDK
47
+ Pod entries and runs `pod install` if Pods were previously installed. Other Pods
48
+ are preserved. Manually declared SDK Pods or other Pods that depend on the same
49
+ native SDK must be migrated first to avoid duplicate linking. Set the value to
50
+ `cocoapods` to switch back, then run `pod install` as usual.
51
+
52
+ Creator 3 CMake regeneration restores the package integration automatically during
53
+ Xcode builds. If you regenerate the project by running CMake separately, rerun
54
+ Creator's native build integration before opening Xcode. Hybrid examples use the
55
+ same configuration with their existing `native:install` command.
56
+
21
57
  ## Examples
22
58
 
23
59
  [Guance Cloud SDK Cocos Creator Demo](https://github.com/GuanceCloud/datakit-cocos/tree/main/examples)
package/bin/cli.cjs CHANGED
@@ -28,6 +28,16 @@ const container = creatorMajor === 2 ? 'packages' : 'extensions';
28
28
  const destination = path.join(projectRoot, container, 'guance-cocos-sdk');
29
29
  const legacyDestination = path.join(projectRoot, container, 'ft-cocos-sdk');
30
30
  const extensionSource = path.join(packageRoot, 'extensions', `creator${creatorMajor}`);
31
+ const manager = readOption(rawArguments, '--ios-dependency-manager');
32
+ if (manager !== undefined && !['spm', 'cocoapods'].includes(manager)) {
33
+ fail('Invalid --ios-dependency-manager. Expected spm or cocoapods.');
34
+ }
35
+ const configFile = path.join(projectRoot, 'cocos-sdk.config.json');
36
+ const config = readJson(configFile) || {};
37
+ if (manager !== undefined) {
38
+ config.ios = { ...config.ios, dependencyManager: manager };
39
+ fs.writeFileSync(configFile, `${JSON.stringify(config, null, 2)}\n`);
40
+ }
31
41
 
32
42
  copyDirectory(extensionSource, destination);
33
43
  copyDirectory(path.join(packageRoot, 'native'), path.join(destination, 'native'));
@@ -119,6 +129,7 @@ function printHelp() {
119
129
  'Options:',
120
130
  ' --project <path> Cocos project root (defaults to the current directory)',
121
131
  ' --creator <2|3> Override automatic Cocos Creator version detection',
132
+ ' --ios-dependency-manager <spm|cocoapods> Save the iOS dependency manager in cocos-sdk.config.json',
122
133
  ' -h, --help Show this help',
123
134
  '',
124
135
  ].join('\n'));
@@ -1,5 +1,5 @@
1
1
  /** Version of the installed Cocos SDK package. */
2
- export declare const FT_COCOS_SDK_VERSION = "0.1.0-alpha.4";
2
+ export declare const FT_COCOS_SDK_VERSION = "0.1.0-alpha.5";
3
3
  /** Global-context key used to report the Cocos SDK package version. */
4
4
  export declare const FT_COCOS_SDK_VERSION_KEY = "sdk_package_cocos";
5
5
  /** Event attribute used to identify data emitted through a framework bridge. */
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FT_COCOS_SDK_BRIDGE_INFO = exports.FT_COCOS_SDK_BRIDGE_INFO_KEY = exports.FT_COCOS_SDK_VERSION_KEY = exports.FT_COCOS_SDK_VERSION = void 0;
4
4
  /** Version of the installed Cocos SDK package. */
5
- exports.FT_COCOS_SDK_VERSION = '0.1.0-alpha.4';
5
+ exports.FT_COCOS_SDK_VERSION = '0.1.0-alpha.5';
6
6
  /** Global-context key used to report the Cocos SDK package version. */
7
7
  exports.FT_COCOS_SDK_VERSION_KEY = 'sdk_package_cocos';
8
8
  /** Event attribute used to identify data emitted through a framework bridge. */
@@ -1,5 +1,5 @@
1
1
  /** Version of the installed Cocos SDK package. */
2
- export declare const FT_COCOS_SDK_VERSION = "0.1.0-alpha.4";
2
+ export declare const FT_COCOS_SDK_VERSION = "0.1.0-alpha.5";
3
3
  /** Global-context key used to report the Cocos SDK package version. */
4
4
  export declare const FT_COCOS_SDK_VERSION_KEY = "sdk_package_cocos";
5
5
  /** Event attribute used to identify data emitted through a framework bridge. */
@@ -1,5 +1,5 @@
1
1
  /** Version of the installed Cocos SDK package. */
2
- export const FT_COCOS_SDK_VERSION = '0.1.0-alpha.4';
2
+ export const FT_COCOS_SDK_VERSION = '0.1.0-alpha.5';
3
3
  /** Global-context key used to report the Cocos SDK package version. */
4
4
  export const FT_COCOS_SDK_VERSION_KEY = 'sdk_package_cocos';
5
5
  /** Event attribute used to identify data emitted through a framework bridge. */
@@ -22,6 +22,7 @@ function installNative(buildRoot, extensionRoot, logger = console) {
22
22
  buildRoot = path.resolve(buildRoot);
23
23
  extensionRoot = path.resolve(extensionRoot);
24
24
  if (!fs.existsSync(buildRoot)) return;
25
+ const dependencyManager = readIosDependencyManager(buildRoot, extensionRoot);
25
26
  const nativeSource = path.join(extensionRoot, 'native');
26
27
  if (!fs.existsSync(nativeSource)) {
27
28
  logger.warn(`[cocos-sdk] Native bridge not found at ${nativeSource}`);
@@ -47,7 +48,11 @@ function installNative(buildRoot, extensionRoot, logger = console) {
47
48
  const iosApplicationProjects = xcodeProjects
48
49
  .map((projectFile) => ({ projectFile, target: findIosApplicationTarget(projectFile) }))
49
50
  .filter((project) => project.target);
50
- if (podfiles.length === 0) {
51
+ if (dependencyManager === 'spm' && iosApplicationProjects.length > 0) {
52
+ migrateManagedPods(podfiles, logger);
53
+ copyDirectory(path.join(nativeSource, 'ios'), path.join(buildNative, 'FTCocosBridge'));
54
+ }
55
+ if (dependencyManager === 'cocoapods' && podfiles.length === 0) {
51
56
  iosApplicationProjects.forEach(({ projectFile, target }) => {
52
57
  const projectDirectory = path.dirname(path.dirname(projectFile));
53
58
  const podfile = path.join(projectDirectory, 'Podfile');
@@ -57,12 +62,17 @@ function installNative(buildRoot, extensionRoot, logger = console) {
57
62
  }
58
63
  iosApplicationProjects.forEach(({ projectFile, target }) => {
59
64
  patchCocos2IosConfiguration(projectFile, target);
65
+ if (dependencyManager === 'spm') {
66
+ require('./install-spm.cjs').installSwiftPackage(projectFile, path.join(buildNative, 'FTCocosBridge'), 'FTCocosBridge', target);
67
+ } else if (fs.readFileSync(projectFile, 'utf8').includes('XCLocalSwiftPackageReference')) {
68
+ require('./install-spm.cjs').removeSwiftPackages(projectFile);
69
+ }
60
70
  });
61
71
  gradleFiles.forEach((file) => patchGradle(file, buildNative));
62
72
  gradlePropertiesFiles.forEach(patchGradleProperties);
63
- podfiles.forEach((file) => patchPodfile(file, buildNative));
73
+ if (dependencyManager === 'cocoapods') podfiles.forEach((file) => patchPodfile(file, buildNative));
64
74
  logger.info(
65
- `[cocos-sdk] Installed native bridge (${gradleFiles.length} Android, ${podfiles.length} iOS project files).`,
75
+ `[cocos-sdk] Installed native bridge (${gradleFiles.length} Android, ${dependencyManager === 'spm' ? iosApplicationProjects.length : podfiles.length} iOS project files; iOS: ${dependencyManager}).`,
66
76
  );
67
77
  }
68
78
 
@@ -252,7 +262,7 @@ function patchGradle(file, nativeRoot) {
252
262
  " maven { url 'https://mvnrepo.guance.com/repository/maven-releases' }",
253
263
  '}',
254
264
  'dependencies {',
255
- " implementation 'com.cloudcare.ft.mobile.sdk.tracker.agent:ft-sdk:1.7.6-alpha02'",
265
+ " implementation 'com.cloudcare.ft.mobile.sdk.tracker.agent:ft-sdk:1.7.6-alpha03'",
256
266
  " implementation 'com.cloudcare.ft.mobile.sdk.tracker.agent:ft-native:1.1.3'",
257
267
  " implementation 'com.cloudcare.ft.mobile.sdk.tracker.agent:ft-session-replay:0.1.9-alpha03'",
258
268
  " implementation 'com.google.code.gson:gson:2.10.1'",
@@ -298,6 +308,10 @@ function replaceIndentedMarkedBlock(contents, begin, end, block) {
298
308
 
299
309
  function findIosApplicationTarget(projectFile) {
300
310
  const contents = fs.readFileSync(projectFile, 'utf8');
311
+ if (contents.includes('COCOS_SDK_XCODE_PROJECT')) {
312
+ const { readProject, applicationTarget } = require('./install-spm.cjs');
313
+ return applicationTarget(readProject(projectFile))?.[1].name || null;
314
+ }
301
315
  const nativeTarget = /\/\* ([^*]+) \*\/ = \{\s*isa = PBXNativeTarget;([\s\S]*?)\n\s*\};/g;
302
316
  const targets = [];
303
317
  let match;
@@ -382,4 +396,58 @@ function escapeGroovy(value) { return value.replace(/\\/g, '\\\\').replace(/'/g,
382
396
  function escapeRuby(value) { return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); }
383
397
  function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
384
398
 
385
- module.exports = { installNative };
399
+ function readIosDependencyManager(buildRoot, extensionRoot) {
400
+ for (const start of [buildRoot, extensionRoot]) {
401
+ let directory = path.resolve(start);
402
+ while (true) {
403
+ const file = path.join(directory, 'cocos-sdk.config.json');
404
+ if (fs.existsSync(file)) {
405
+ const config = JSON.parse(fs.readFileSync(file, 'utf8'));
406
+ const manager = config.ios?.dependencyManager ?? 'cocoapods';
407
+ if (!['spm', 'cocoapods'].includes(manager)) {
408
+ throw new Error(`[cocos-sdk] Invalid ios.dependencyManager in ${file}: expected spm or cocoapods.`);
409
+ }
410
+ return manager;
411
+ }
412
+ const parent = path.dirname(directory);
413
+ if (parent === directory) break;
414
+ directory = parent;
415
+ }
416
+ }
417
+ return 'cocoapods';
418
+ }
419
+
420
+ function migrateManagedPods(podfiles, logger) {
421
+ for (const file of podfiles) {
422
+ const original = fs.readFileSync(file, 'utf8');
423
+ let next = original;
424
+ for (const [begin, end] of [
425
+ [POD_BEGIN, POD_END], [LEGACY_POD_BEGIN, LEGACY_POD_END], [LEGACY_BEGIN, LEGACY_END],
426
+ ['# COCOS_HYBRID_SAMPLE_BEGIN', '# COCOS_HYBRID_SAMPLE_END'],
427
+ ]) next = next.replace(markedPattern(begin, end), '');
428
+ if (/^\s*pod\s+['"](?:FTCocosBridge|HybridSampleHost|GuanceSDK)(?:\/[^'"]*)?['"]/m.test(next)) {
429
+ throw new Error(`[cocos-sdk] Remove the manually declared native SDK Pod from ${file} before switching to SPM.`);
430
+ }
431
+ if (next !== original) fs.writeFileSync(file, next);
432
+ const directory = path.dirname(file);
433
+ const existingLock = path.join(directory, 'Podfile.lock');
434
+ const hasSDKPods = fs.existsSync(existingLock)
435
+ && /^\s*- (?:FTCocosBridge|HybridSampleHost|GuanceSDK)(?:\/|\s|:)/m.test(fs.readFileSync(existingLock, 'utf8'));
436
+ if (next === original && !hasSDKPods) continue;
437
+ if (fs.existsSync(path.join(directory, 'Podfile.lock')) || fs.existsSync(path.join(directory, 'Pods'))) {
438
+ logger.info('[cocos-sdk] Updating existing Pods integration for the SPM switch.');
439
+ try {
440
+ execFileSync('pod', ['install'], { cwd: directory, encoding: 'utf8', maxBuffer: 4 * 1024 * 1024 });
441
+ } catch (error) {
442
+ fs.writeFileSync(file, original);
443
+ throw new Error(`[cocos-sdk] Unable to update existing Pods in ${directory}: ${error.message}`);
444
+ }
445
+ const lock = path.join(directory, 'Podfile.lock');
446
+ if (fs.existsSync(lock) && /^\s*- GuanceSDK(?:\/|\s|:)/m.test(fs.readFileSync(lock, 'utf8'))) {
447
+ throw new Error(`[cocos-sdk] Another Pod still depends on the native SDK in ${lock}; migrate that dependency before using SPM.`);
448
+ }
449
+ }
450
+ }
451
+ }
452
+
453
+ module.exports = { installNative, readIosDependencyManager };
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { createHash } = require('crypto');
6
+ const { execFileSync } = require('child_process');
7
+
8
+ function readProject(file) {
9
+ // plutil understands both Xcode's OpenStep format and XML plists, without Ruby/gems.
10
+ return JSON.parse(execFileSync('/usr/bin/plutil', ['-convert', 'json', '-o', '-', file], {
11
+ encoding: 'utf8', maxBuffer: 32 * 1024 * 1024,
12
+ }));
13
+ }
14
+
15
+ function writeProject(file, project) {
16
+ const next = `// !$*UTF8*$!\n// COCOS_SDK_XCODE_PROJECT\n${serialize(project)}\n`;
17
+ if (fs.readFileSync(file, 'utf8') !== next) fs.writeFileSync(file, next);
18
+ }
19
+
20
+ function serialize(value, depth = 0) {
21
+ const indent = '\t'.repeat(depth);
22
+ if (Array.isArray(value)) {
23
+ return `(\n${value.map((entry) => `${indent}\t${serialize(entry, depth + 1)},\n`).join('')}${indent})`;
24
+ }
25
+ if (value && typeof value === 'object') {
26
+ return `{\n${Object.entries(value).sort(([a], [b]) => a === 'isa' ? -1 : b === 'isa' ? 1 : a.localeCompare(b)).map(([key, entry]) => (
27
+ `${indent}\t${serialize(key)} = ${serialize(entry, depth + 1)};\n`
28
+ )).join('')}${indent}}`;
29
+ }
30
+ const text = String(value);
31
+ return /^[A-Za-z0-9_./]+$/.test(text) ? text : JSON.stringify(text);
32
+ }
33
+
34
+ function applicationTarget(project, preferredName) {
35
+ const targets = Object.entries(project.objects).filter(([, object]) => (
36
+ object.isa === 'PBXNativeTarget' && object.productType === 'com.apple.product-type.application'
37
+ ));
38
+ return targets.find(([, object]) => object.name === preferredName)
39
+ || targets.find(([, object]) => /(?:^|[-_.])(?:mobile|ios)$/i.test(object.name))
40
+ || targets[0];
41
+ }
42
+
43
+ function add(list, value) {
44
+ if (!list.includes(value)) list.push(value);
45
+ }
46
+
47
+ function identifier(value) {
48
+ return createHash('sha256').update(`cocos-sdk:${value}`).digest('hex').slice(0, 24).toUpperCase();
49
+ }
50
+
51
+ function installSwiftPackage(projectFile, packageDirectory, productName, targetName) {
52
+ if (!fs.existsSync(path.join(packageDirectory, 'Package.swift'))) {
53
+ throw new Error(`[cocos-sdk] Missing Swift package: ${packageDirectory}`);
54
+ }
55
+ const project = readProject(projectFile);
56
+ const selected = applicationTarget(project, targetName);
57
+ if (!selected) throw new Error(`[cocos-sdk] No application target in ${projectFile}`);
58
+ const [targetID, target] = selected;
59
+ const objects = project.objects;
60
+ const root = objects[project.rootObject];
61
+ const projectDirectory = path.resolve(path.dirname(path.dirname(projectFile)), root.projectDirPath || '.');
62
+ const relativePath = path.relative(projectDirectory, packageDirectory).split(path.sep).join('/');
63
+ const packageID = identifier(`package:${productName}`);
64
+ const productID = identifier(`product:${targetID}:${productName}`);
65
+ const buildID = identifier(`framework:${targetID}:${productName}`);
66
+ const existing = (target.packageProductDependencies || []).find((id) => (
67
+ id !== productID && objects[id]?.productName === productName
68
+ ));
69
+ if (existing) throw new Error(`[cocos-sdk] ${productName} is already managed manually in ${target.name}; remove that reference before automatic installation.`);
70
+
71
+ objects[packageID] = { isa: 'XCLocalSwiftPackageReference', relativePath };
72
+ objects[productID] = { isa: 'XCSwiftPackageProductDependency', productName };
73
+ objects[buildID] = { isa: 'PBXBuildFile', productRef: productID };
74
+ add(root.packageReferences ||= [], packageID);
75
+ add(target.packageProductDependencies ||= [], productID);
76
+ let phaseID = (target.buildPhases || []).find((id) => objects[id]?.isa === 'PBXFrameworksBuildPhase');
77
+ if (!phaseID) {
78
+ phaseID = identifier(`frameworks:${targetID}`);
79
+ objects[phaseID] = { isa: 'PBXFrameworksBuildPhase', buildActionMask: '2147483647', files: [], runOnlyForDeploymentPostprocessing: '0' };
80
+ add(target.buildPhases ||= [], phaseID);
81
+ }
82
+ add(objects[phaseID].files ||= [], buildID);
83
+ const configurations = objects[target.buildConfigurationList]?.buildConfigurations || [];
84
+ for (const configID of configurations) {
85
+ const settings = objects[configID].buildSettings ||= {};
86
+ const flags = settings.OTHER_LDFLAGS;
87
+ if (Array.isArray(flags)) {
88
+ add(flags, '$(inherited)');
89
+ add(flags, '-ObjC');
90
+ } else {
91
+ const parts = [flags || '$(inherited)'];
92
+ if (!String(flags || '').includes('$(inherited)')) add(parts, '$(inherited)');
93
+ if (!/(?:^|\s)-ObjC(?:\s|$)/.test(flags || '')) add(parts, '-ObjC');
94
+ settings.OTHER_LDFLAGS = parts.join(' ');
95
+ }
96
+ if (Number(settings.IPHONEOS_DEPLOYMENT_TARGET || 0) < 12) settings.IPHONEOS_DEPLOYMENT_TARGET = '12.0';
97
+ settings.CLANG_ENABLE_MODULES = 'YES';
98
+ }
99
+ preserveCmakeIntegration(projectFile, project, { packageDirectory, productName, targetName: target.name });
100
+ writeProject(projectFile, project);
101
+ }
102
+
103
+ const RESTORE_BEGIN = '# COCOS_SDK_SPM_RESTORE_BEGIN';
104
+ const RESTORE_END = '# COCOS_SDK_SPM_RESTORE_END';
105
+ const restorePattern = /\n?# COCOS_SDK_SPM_RESTORE_BEGIN[\s\S]*?# COCOS_SDK_SPM_RESTORE_END\n?/g;
106
+
107
+ function shellQuote(value) {
108
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
109
+ }
110
+
111
+ function preserveCmakeIntegration(projectFile, project, entry) {
112
+ const objects = project.objects;
113
+ const zeroCheck = Object.values(objects).find((object) => object.name === 'ZERO_CHECK');
114
+ if (!zeroCheck) return;
115
+ // CMake's SYMROOT/OBJROOT overrides select Xcode's legacy build locations,
116
+ // which reject packages. Keep CONFIGURATION_BUILD_DIR (engine archives).
117
+ const stateFile = path.join(path.dirname(projectFile), 'cocos-sdk-spm.json');
118
+ const state = fs.existsSync(stateFile)
119
+ ? JSON.parse(fs.readFileSync(stateFile, 'utf8')) : { packages: [], buildLocations: {} };
120
+ for (const [id, object] of Object.entries(objects)) {
121
+ if (object.isa !== 'XCBuildConfiguration') continue;
122
+ for (const key of ['SYMROOT', 'OBJROOT']) {
123
+ if (object.buildSettings?.[key] === undefined) continue;
124
+ (state.buildLocations[id] ||= {})[key] = object.buildSettings[key];
125
+ delete object.buildSettings[key];
126
+ }
127
+ }
128
+ const relativeDirectory = path.relative(path.dirname(projectFile), entry.packageDirectory);
129
+ state.packages = state.packages.filter((item) => item.productName !== entry.productName);
130
+ state.packages.push({ ...entry, packageDirectory: relativeDirectory });
131
+ state.packages.sort((a, b) => a.productName.localeCompare(b.productName));
132
+ const helper = path.join(path.dirname(projectFile), 'cocos-sdk-spm.cjs');
133
+ if (path.resolve(helper) !== path.resolve(__filename)) fs.copyFileSync(__filename, helper);
134
+ const restore = [
135
+ RESTORE_BEGIN,
136
+ `${process.versions.electron ? 'ELECTRON_RUN_AS_NODE=1 ' : ''}${shellQuote(process.execPath)} ${shellQuote(helper)} --restore`,
137
+ RESTORE_END,
138
+ ].join('\n');
139
+ for (const id of zeroCheck.buildPhases || []) {
140
+ const phase = objects[id];
141
+ if (phase?.isa !== 'PBXShellScriptBuildPhase') continue;
142
+ phase.shellScript = `${(phase.shellScript || '').replace(restorePattern, '').trimEnd()}\n${restore}\n`;
143
+ }
144
+ const serialized = `${JSON.stringify(state, null, 2)}\n`;
145
+ if (!fs.existsSync(stateFile) || fs.readFileSync(stateFile, 'utf8') !== serialized) fs.writeFileSync(stateFile, serialized);
146
+ }
147
+
148
+ function removeSwiftPackages(projectFile) {
149
+ const project = readProject(projectFile);
150
+ const objects = project.objects;
151
+ const removed = new Set();
152
+ for (const productName of ['FTCocosBridge', 'HybridSampleHost']) {
153
+ const packageID = identifier(`package:${productName}`);
154
+ if (!objects[packageID]) continue;
155
+ removed.add(packageID);
156
+ for (const [id, object] of Object.entries(objects)) {
157
+ if (object.isa !== 'PBXNativeTarget') continue;
158
+ removed.add(identifier(`product:${id}:${productName}`));
159
+ removed.add(identifier(`framework:${id}:${productName}`));
160
+ }
161
+ }
162
+ if (!removed.size) return;
163
+ for (const id of removed) delete objects[id];
164
+ for (const object of Object.values(objects)) {
165
+ for (const key of ['packageReferences', 'packageProductDependencies', 'files']) {
166
+ if (Array.isArray(object[key])) object[key] = object[key].filter((id) => !removed.has(id));
167
+ }
168
+ if (object.isa === 'PBXShellScriptBuildPhase') object.shellScript = (object.shellScript || '').replace(restorePattern, '');
169
+ }
170
+ const stateFile = path.join(path.dirname(projectFile), 'cocos-sdk-spm.json');
171
+ if (fs.existsSync(stateFile)) {
172
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
173
+ for (const [id, settings] of Object.entries(state.buildLocations)) {
174
+ if (objects[id]) Object.assign(objects[id].buildSettings ||= {}, settings);
175
+ }
176
+ fs.unlinkSync(stateFile);
177
+ fs.rmSync(path.join(path.dirname(projectFile), 'cocos-sdk-spm.cjs'), { force: true });
178
+ }
179
+ writeProject(projectFile, project);
180
+ }
181
+
182
+ module.exports = { installSwiftPackage, removeSwiftPackages, readProject, applicationTarget };
183
+
184
+ if (require.main === module && process.argv[2] === '--restore') {
185
+ const directory = path.dirname(__filename);
186
+ const state = JSON.parse(fs.readFileSync(path.join(directory, 'cocos-sdk-spm.json'), 'utf8'));
187
+ for (const item of state.packages) {
188
+ installSwiftPackage(path.join(directory, 'project.pbxproj'), path.resolve(directory, item.packageDirectory), item.productName, item.targetName);
189
+ }
190
+ }
@@ -22,6 +22,7 @@ function installNative(buildRoot, extensionRoot, logger = console) {
22
22
  buildRoot = path.resolve(buildRoot);
23
23
  extensionRoot = path.resolve(extensionRoot);
24
24
  if (!fs.existsSync(buildRoot)) return;
25
+ const dependencyManager = readIosDependencyManager(buildRoot, extensionRoot);
25
26
  const nativeSource = path.join(extensionRoot, 'native');
26
27
  if (!fs.existsSync(nativeSource)) {
27
28
  logger.warn(`[cocos-sdk] Native bridge not found at ${nativeSource}`);
@@ -47,7 +48,11 @@ function installNative(buildRoot, extensionRoot, logger = console) {
47
48
  const iosApplicationProjects = xcodeProjects
48
49
  .map((projectFile) => ({ projectFile, target: findIosApplicationTarget(projectFile) }))
49
50
  .filter((project) => project.target);
50
- if (podfiles.length === 0) {
51
+ if (dependencyManager === 'spm' && iosApplicationProjects.length > 0) {
52
+ migrateManagedPods(podfiles, logger);
53
+ copyDirectory(path.join(nativeSource, 'ios'), path.join(buildNative, 'FTCocosBridge'));
54
+ }
55
+ if (dependencyManager === 'cocoapods' && podfiles.length === 0) {
51
56
  iosApplicationProjects.forEach(({ projectFile, target }) => {
52
57
  const projectDirectory = path.dirname(path.dirname(projectFile));
53
58
  const podfile = path.join(projectDirectory, 'Podfile');
@@ -57,12 +62,17 @@ function installNative(buildRoot, extensionRoot, logger = console) {
57
62
  }
58
63
  iosApplicationProjects.forEach(({ projectFile, target }) => {
59
64
  patchCocos2IosConfiguration(projectFile, target);
65
+ if (dependencyManager === 'spm') {
66
+ require('./install-spm.cjs').installSwiftPackage(projectFile, path.join(buildNative, 'FTCocosBridge'), 'FTCocosBridge', target);
67
+ } else if (fs.readFileSync(projectFile, 'utf8').includes('XCLocalSwiftPackageReference')) {
68
+ require('./install-spm.cjs').removeSwiftPackages(projectFile);
69
+ }
60
70
  });
61
71
  gradleFiles.forEach((file) => patchGradle(file, buildNative));
62
72
  gradlePropertiesFiles.forEach(patchGradleProperties);
63
- podfiles.forEach((file) => patchPodfile(file, buildNative));
73
+ if (dependencyManager === 'cocoapods') podfiles.forEach((file) => patchPodfile(file, buildNative));
64
74
  logger.info(
65
- `[cocos-sdk] Installed native bridge (${gradleFiles.length} Android, ${podfiles.length} iOS project files).`,
75
+ `[cocos-sdk] Installed native bridge (${gradleFiles.length} Android, ${dependencyManager === 'spm' ? iosApplicationProjects.length : podfiles.length} iOS project files; iOS: ${dependencyManager}).`,
66
76
  );
67
77
  }
68
78
 
@@ -252,7 +262,7 @@ function patchGradle(file, nativeRoot) {
252
262
  " maven { url 'https://mvnrepo.guance.com/repository/maven-releases' }",
253
263
  '}',
254
264
  'dependencies {',
255
- " implementation 'com.cloudcare.ft.mobile.sdk.tracker.agent:ft-sdk:1.7.6-alpha02'",
265
+ " implementation 'com.cloudcare.ft.mobile.sdk.tracker.agent:ft-sdk:1.7.6-alpha03'",
256
266
  " implementation 'com.cloudcare.ft.mobile.sdk.tracker.agent:ft-native:1.1.3'",
257
267
  " implementation 'com.cloudcare.ft.mobile.sdk.tracker.agent:ft-session-replay:0.1.9-alpha03'",
258
268
  " implementation 'com.google.code.gson:gson:2.10.1'",
@@ -298,6 +308,10 @@ function replaceIndentedMarkedBlock(contents, begin, end, block) {
298
308
 
299
309
  function findIosApplicationTarget(projectFile) {
300
310
  const contents = fs.readFileSync(projectFile, 'utf8');
311
+ if (contents.includes('COCOS_SDK_XCODE_PROJECT')) {
312
+ const { readProject, applicationTarget } = require('./install-spm.cjs');
313
+ return applicationTarget(readProject(projectFile))?.[1].name || null;
314
+ }
301
315
  const nativeTarget = /\/\* ([^*]+) \*\/ = \{\s*isa = PBXNativeTarget;([\s\S]*?)\n\s*\};/g;
302
316
  const targets = [];
303
317
  let match;
@@ -382,4 +396,58 @@ function escapeGroovy(value) { return value.replace(/\\/g, '\\\\').replace(/'/g,
382
396
  function escapeRuby(value) { return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); }
383
397
  function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
384
398
 
385
- module.exports = { installNative };
399
+ function readIosDependencyManager(buildRoot, extensionRoot) {
400
+ for (const start of [buildRoot, extensionRoot]) {
401
+ let directory = path.resolve(start);
402
+ while (true) {
403
+ const file = path.join(directory, 'cocos-sdk.config.json');
404
+ if (fs.existsSync(file)) {
405
+ const config = JSON.parse(fs.readFileSync(file, 'utf8'));
406
+ const manager = config.ios?.dependencyManager ?? 'cocoapods';
407
+ if (!['spm', 'cocoapods'].includes(manager)) {
408
+ throw new Error(`[cocos-sdk] Invalid ios.dependencyManager in ${file}: expected spm or cocoapods.`);
409
+ }
410
+ return manager;
411
+ }
412
+ const parent = path.dirname(directory);
413
+ if (parent === directory) break;
414
+ directory = parent;
415
+ }
416
+ }
417
+ return 'cocoapods';
418
+ }
419
+
420
+ function migrateManagedPods(podfiles, logger) {
421
+ for (const file of podfiles) {
422
+ const original = fs.readFileSync(file, 'utf8');
423
+ let next = original;
424
+ for (const [begin, end] of [
425
+ [POD_BEGIN, POD_END], [LEGACY_POD_BEGIN, LEGACY_POD_END], [LEGACY_BEGIN, LEGACY_END],
426
+ ['# COCOS_HYBRID_SAMPLE_BEGIN', '# COCOS_HYBRID_SAMPLE_END'],
427
+ ]) next = next.replace(markedPattern(begin, end), '');
428
+ if (/^\s*pod\s+['"](?:FTCocosBridge|HybridSampleHost|GuanceSDK)(?:\/[^'"]*)?['"]/m.test(next)) {
429
+ throw new Error(`[cocos-sdk] Remove the manually declared native SDK Pod from ${file} before switching to SPM.`);
430
+ }
431
+ if (next !== original) fs.writeFileSync(file, next);
432
+ const directory = path.dirname(file);
433
+ const existingLock = path.join(directory, 'Podfile.lock');
434
+ const hasSDKPods = fs.existsSync(existingLock)
435
+ && /^\s*- (?:FTCocosBridge|HybridSampleHost|GuanceSDK)(?:\/|\s|:)/m.test(fs.readFileSync(existingLock, 'utf8'));
436
+ if (next === original && !hasSDKPods) continue;
437
+ if (fs.existsSync(path.join(directory, 'Podfile.lock')) || fs.existsSync(path.join(directory, 'Pods'))) {
438
+ logger.info('[cocos-sdk] Updating existing Pods integration for the SPM switch.');
439
+ try {
440
+ execFileSync('pod', ['install'], { cwd: directory, encoding: 'utf8', maxBuffer: 4 * 1024 * 1024 });
441
+ } catch (error) {
442
+ fs.writeFileSync(file, original);
443
+ throw new Error(`[cocos-sdk] Unable to update existing Pods in ${directory}: ${error.message}`);
444
+ }
445
+ const lock = path.join(directory, 'Podfile.lock');
446
+ if (fs.existsSync(lock) && /^\s*- GuanceSDK(?:\/|\s|:)/m.test(fs.readFileSync(lock, 'utf8'))) {
447
+ throw new Error(`[cocos-sdk] Another Pod still depends on the native SDK in ${lock}; migrate that dependency before using SPM.`);
448
+ }
449
+ }
450
+ }
451
+ }
452
+
453
+ module.exports = { installNative, readIosDependencyManager };
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { createHash } = require('crypto');
6
+ const { execFileSync } = require('child_process');
7
+
8
+ function readProject(file) {
9
+ // plutil understands both Xcode's OpenStep format and XML plists, without Ruby/gems.
10
+ return JSON.parse(execFileSync('/usr/bin/plutil', ['-convert', 'json', '-o', '-', file], {
11
+ encoding: 'utf8', maxBuffer: 32 * 1024 * 1024,
12
+ }));
13
+ }
14
+
15
+ function writeProject(file, project) {
16
+ const next = `// !$*UTF8*$!\n// COCOS_SDK_XCODE_PROJECT\n${serialize(project)}\n`;
17
+ if (fs.readFileSync(file, 'utf8') !== next) fs.writeFileSync(file, next);
18
+ }
19
+
20
+ function serialize(value, depth = 0) {
21
+ const indent = '\t'.repeat(depth);
22
+ if (Array.isArray(value)) {
23
+ return `(\n${value.map((entry) => `${indent}\t${serialize(entry, depth + 1)},\n`).join('')}${indent})`;
24
+ }
25
+ if (value && typeof value === 'object') {
26
+ return `{\n${Object.entries(value).sort(([a], [b]) => a === 'isa' ? -1 : b === 'isa' ? 1 : a.localeCompare(b)).map(([key, entry]) => (
27
+ `${indent}\t${serialize(key)} = ${serialize(entry, depth + 1)};\n`
28
+ )).join('')}${indent}}`;
29
+ }
30
+ const text = String(value);
31
+ return /^[A-Za-z0-9_./]+$/.test(text) ? text : JSON.stringify(text);
32
+ }
33
+
34
+ function applicationTarget(project, preferredName) {
35
+ const targets = Object.entries(project.objects).filter(([, object]) => (
36
+ object.isa === 'PBXNativeTarget' && object.productType === 'com.apple.product-type.application'
37
+ ));
38
+ return targets.find(([, object]) => object.name === preferredName)
39
+ || targets.find(([, object]) => /(?:^|[-_.])(?:mobile|ios)$/i.test(object.name))
40
+ || targets[0];
41
+ }
42
+
43
+ function add(list, value) {
44
+ if (!list.includes(value)) list.push(value);
45
+ }
46
+
47
+ function identifier(value) {
48
+ return createHash('sha256').update(`cocos-sdk:${value}`).digest('hex').slice(0, 24).toUpperCase();
49
+ }
50
+
51
+ function installSwiftPackage(projectFile, packageDirectory, productName, targetName) {
52
+ if (!fs.existsSync(path.join(packageDirectory, 'Package.swift'))) {
53
+ throw new Error(`[cocos-sdk] Missing Swift package: ${packageDirectory}`);
54
+ }
55
+ const project = readProject(projectFile);
56
+ const selected = applicationTarget(project, targetName);
57
+ if (!selected) throw new Error(`[cocos-sdk] No application target in ${projectFile}`);
58
+ const [targetID, target] = selected;
59
+ const objects = project.objects;
60
+ const root = objects[project.rootObject];
61
+ const projectDirectory = path.resolve(path.dirname(path.dirname(projectFile)), root.projectDirPath || '.');
62
+ const relativePath = path.relative(projectDirectory, packageDirectory).split(path.sep).join('/');
63
+ const packageID = identifier(`package:${productName}`);
64
+ const productID = identifier(`product:${targetID}:${productName}`);
65
+ const buildID = identifier(`framework:${targetID}:${productName}`);
66
+ const existing = (target.packageProductDependencies || []).find((id) => (
67
+ id !== productID && objects[id]?.productName === productName
68
+ ));
69
+ if (existing) throw new Error(`[cocos-sdk] ${productName} is already managed manually in ${target.name}; remove that reference before automatic installation.`);
70
+
71
+ objects[packageID] = { isa: 'XCLocalSwiftPackageReference', relativePath };
72
+ objects[productID] = { isa: 'XCSwiftPackageProductDependency', productName };
73
+ objects[buildID] = { isa: 'PBXBuildFile', productRef: productID };
74
+ add(root.packageReferences ||= [], packageID);
75
+ add(target.packageProductDependencies ||= [], productID);
76
+ let phaseID = (target.buildPhases || []).find((id) => objects[id]?.isa === 'PBXFrameworksBuildPhase');
77
+ if (!phaseID) {
78
+ phaseID = identifier(`frameworks:${targetID}`);
79
+ objects[phaseID] = { isa: 'PBXFrameworksBuildPhase', buildActionMask: '2147483647', files: [], runOnlyForDeploymentPostprocessing: '0' };
80
+ add(target.buildPhases ||= [], phaseID);
81
+ }
82
+ add(objects[phaseID].files ||= [], buildID);
83
+ const configurations = objects[target.buildConfigurationList]?.buildConfigurations || [];
84
+ for (const configID of configurations) {
85
+ const settings = objects[configID].buildSettings ||= {};
86
+ const flags = settings.OTHER_LDFLAGS;
87
+ if (Array.isArray(flags)) {
88
+ add(flags, '$(inherited)');
89
+ add(flags, '-ObjC');
90
+ } else {
91
+ const parts = [flags || '$(inherited)'];
92
+ if (!String(flags || '').includes('$(inherited)')) add(parts, '$(inherited)');
93
+ if (!/(?:^|\s)-ObjC(?:\s|$)/.test(flags || '')) add(parts, '-ObjC');
94
+ settings.OTHER_LDFLAGS = parts.join(' ');
95
+ }
96
+ if (Number(settings.IPHONEOS_DEPLOYMENT_TARGET || 0) < 12) settings.IPHONEOS_DEPLOYMENT_TARGET = '12.0';
97
+ settings.CLANG_ENABLE_MODULES = 'YES';
98
+ }
99
+ preserveCmakeIntegration(projectFile, project, { packageDirectory, productName, targetName: target.name });
100
+ writeProject(projectFile, project);
101
+ }
102
+
103
+ const RESTORE_BEGIN = '# COCOS_SDK_SPM_RESTORE_BEGIN';
104
+ const RESTORE_END = '# COCOS_SDK_SPM_RESTORE_END';
105
+ const restorePattern = /\n?# COCOS_SDK_SPM_RESTORE_BEGIN[\s\S]*?# COCOS_SDK_SPM_RESTORE_END\n?/g;
106
+
107
+ function shellQuote(value) {
108
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
109
+ }
110
+
111
+ function preserveCmakeIntegration(projectFile, project, entry) {
112
+ const objects = project.objects;
113
+ const zeroCheck = Object.values(objects).find((object) => object.name === 'ZERO_CHECK');
114
+ if (!zeroCheck) return;
115
+ // CMake's SYMROOT/OBJROOT overrides select Xcode's legacy build locations,
116
+ // which reject packages. Keep CONFIGURATION_BUILD_DIR (engine archives).
117
+ const stateFile = path.join(path.dirname(projectFile), 'cocos-sdk-spm.json');
118
+ const state = fs.existsSync(stateFile)
119
+ ? JSON.parse(fs.readFileSync(stateFile, 'utf8')) : { packages: [], buildLocations: {} };
120
+ for (const [id, object] of Object.entries(objects)) {
121
+ if (object.isa !== 'XCBuildConfiguration') continue;
122
+ for (const key of ['SYMROOT', 'OBJROOT']) {
123
+ if (object.buildSettings?.[key] === undefined) continue;
124
+ (state.buildLocations[id] ||= {})[key] = object.buildSettings[key];
125
+ delete object.buildSettings[key];
126
+ }
127
+ }
128
+ const relativeDirectory = path.relative(path.dirname(projectFile), entry.packageDirectory);
129
+ state.packages = state.packages.filter((item) => item.productName !== entry.productName);
130
+ state.packages.push({ ...entry, packageDirectory: relativeDirectory });
131
+ state.packages.sort((a, b) => a.productName.localeCompare(b.productName));
132
+ const helper = path.join(path.dirname(projectFile), 'cocos-sdk-spm.cjs');
133
+ if (path.resolve(helper) !== path.resolve(__filename)) fs.copyFileSync(__filename, helper);
134
+ const restore = [
135
+ RESTORE_BEGIN,
136
+ `${process.versions.electron ? 'ELECTRON_RUN_AS_NODE=1 ' : ''}${shellQuote(process.execPath)} ${shellQuote(helper)} --restore`,
137
+ RESTORE_END,
138
+ ].join('\n');
139
+ for (const id of zeroCheck.buildPhases || []) {
140
+ const phase = objects[id];
141
+ if (phase?.isa !== 'PBXShellScriptBuildPhase') continue;
142
+ phase.shellScript = `${(phase.shellScript || '').replace(restorePattern, '').trimEnd()}\n${restore}\n`;
143
+ }
144
+ const serialized = `${JSON.stringify(state, null, 2)}\n`;
145
+ if (!fs.existsSync(stateFile) || fs.readFileSync(stateFile, 'utf8') !== serialized) fs.writeFileSync(stateFile, serialized);
146
+ }
147
+
148
+ function removeSwiftPackages(projectFile) {
149
+ const project = readProject(projectFile);
150
+ const objects = project.objects;
151
+ const removed = new Set();
152
+ for (const productName of ['FTCocosBridge', 'HybridSampleHost']) {
153
+ const packageID = identifier(`package:${productName}`);
154
+ if (!objects[packageID]) continue;
155
+ removed.add(packageID);
156
+ for (const [id, object] of Object.entries(objects)) {
157
+ if (object.isa !== 'PBXNativeTarget') continue;
158
+ removed.add(identifier(`product:${id}:${productName}`));
159
+ removed.add(identifier(`framework:${id}:${productName}`));
160
+ }
161
+ }
162
+ if (!removed.size) return;
163
+ for (const id of removed) delete objects[id];
164
+ for (const object of Object.values(objects)) {
165
+ for (const key of ['packageReferences', 'packageProductDependencies', 'files']) {
166
+ if (Array.isArray(object[key])) object[key] = object[key].filter((id) => !removed.has(id));
167
+ }
168
+ if (object.isa === 'PBXShellScriptBuildPhase') object.shellScript = (object.shellScript || '').replace(restorePattern, '');
169
+ }
170
+ const stateFile = path.join(path.dirname(projectFile), 'cocos-sdk-spm.json');
171
+ if (fs.existsSync(stateFile)) {
172
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
173
+ for (const [id, settings] of Object.entries(state.buildLocations)) {
174
+ if (objects[id]) Object.assign(objects[id].buildSettings ||= {}, settings);
175
+ }
176
+ fs.unlinkSync(stateFile);
177
+ fs.rmSync(path.join(path.dirname(projectFile), 'cocos-sdk-spm.cjs'), { force: true });
178
+ }
179
+ writeProject(projectFile, project);
180
+ }
181
+
182
+ module.exports = { installSwiftPackage, removeSwiftPackages, readProject, applicationTarget };
183
+
184
+ if (require.main === module && process.argv[2] === '--restore') {
185
+ const directory = path.dirname(__filename);
186
+ const state = JSON.parse(fs.readFileSync(path.join(directory, 'cocos-sdk-spm.json'), 'utf8'));
187
+ for (const item of state.packages) {
188
+ installSwiftPackage(path.join(directory, 'project.pbxproj'), path.resolve(directory, item.packageDirectory), item.productName, item.targetName);
189
+ }
190
+ }
@@ -1,7 +1,14 @@
1
1
  #import "FTCocosBridge.h"
2
2
  #import "FTCocosReplayImageJobs.h"
3
3
 
4
- #import "FTMobileSDK.h"
4
+ #import "FTMobileAgent.h"
5
+ #import "FTSDKConfig.h"
6
+ #import "FTRumConfig.h"
7
+ #import "FTLoggerConfig.h"
8
+ #import "FTMobileConfig.h"
9
+ #import "FTExternalDataManager.h"
10
+ #import "FTResourceMetricsModel.h"
11
+ #import "FTResourceContentModel.h"
5
12
  #import "FTSessionReplay.h"
6
13
  #import <UIKit/UIKit.h>
7
14
  #import <objc/message.h>
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'FTCocosBridge'
3
- s.version = '0.1.0-alpha.4'
3
+ s.version = '0.1.0-alpha.5'
4
4
  s.summary = 'CloudCare Mobile SDK native bridge for Cocos Creator.'
5
5
  s.homepage = 'https://github.com/GuanceCloud/datakit-cocos'
6
6
  s.license = { :type => 'Apache-2.0' }
@@ -13,6 +13,6 @@ Pod::Spec.new do |s|
13
13
  # Cocos resolves FTCocosBridge by class name at runtime, so no direct symbol
14
14
  # reference would otherwise keep the Objective-C category/class in the app.
15
15
  s.user_target_xcconfig = { 'OTHER_LDFLAGS' => '$(inherited) -ObjC' }
16
- s.dependency 'GuanceSDK/Agent', '1.6.8-alpha.2'
17
- s.dependency 'GuanceSDK/FTSessionReplay', '1.6.8-alpha.2'
16
+ s.dependency 'GuanceSDK/Agent', '1.6.8-alpha.5'
17
+ s.dependency 'GuanceSDK/FTSessionReplay', '1.6.8-alpha.5'
18
18
  end
@@ -0,0 +1,26 @@
1
+ // swift-tools-version:5.3
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "FTCocosBridge",
6
+ platforms: [.iOS(.v12)],
7
+ products: [.library(name: "FTCocosBridge", targets: ["FTCocosBridge"])],
8
+ dependencies: [
9
+ .package(url: "https://github.com/GuanceCloud/datakit-ios.git", .exact("1.6.8-alpha.5")),
10
+ ],
11
+ targets: [
12
+ .target(
13
+ name: "FTCocosBridge",
14
+ dependencies: [
15
+ .product(name: "GuanceSDK", package: "datakit-ios"),
16
+ .product(name: "GuanceSessionReplay", package: "datakit-ios"),
17
+ ],
18
+ path: ".",
19
+ exclude: ["FTCocosBridge.podspec"],
20
+ sources: ["FTCocosBridge.m", "FTCocosReplayImageJobs.m"],
21
+ publicHeadersPath: "include",
22
+ cSettings: [.headerSearchPath(".")],
23
+ linkerSettings: [.linkedFramework("UIKit")]
24
+ ),
25
+ ]
26
+ )
@@ -0,0 +1,2 @@
1
+ // Keep the public SPM header in sync with the CocoaPods/source integration.
2
+ #import "../FTCocosBridge.h"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcare/cocos-sdk",
3
- "version": "0.1.0-alpha.4",
3
+ "version": "0.1.0-alpha.5",
4
4
  "description": "Observability SDK integration for Cocos Creator 2.4 and 3.x",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Guance Cloud",