@cloudcare/cocos-sdk 0.1.0-alpha.3 → 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.
Files changed (77) hide show
  1. package/README.md +37 -1
  2. package/bin/cli.cjs +14 -0
  3. package/components/creator2/ReplayPrivacy.ts +23 -0
  4. package/components/creator3/ReplayPrivacy.ts +25 -0
  5. package/dist/creator2/core/replay-async-transport.d.ts +4 -0
  6. package/dist/creator2/core/replay-async-transport.d.ts.map +1 -0
  7. package/dist/creator2/core/replay-async-transport.js +67 -0
  8. package/dist/creator2/core/replay-async-transport.js.map +1 -0
  9. package/dist/creator2/core/replay-file.d.ts +5 -0
  10. package/dist/creator2/core/replay-file.d.ts.map +1 -0
  11. package/dist/creator2/core/replay-file.js +101 -0
  12. package/dist/creator2/core/replay-file.js.map +1 -0
  13. package/dist/creator2/core/replay-privacy-nodes.d.ts +14 -0
  14. package/dist/creator2/core/replay-privacy-nodes.d.ts.map +1 -0
  15. package/dist/creator2/core/replay-privacy-nodes.js +18 -0
  16. package/dist/creator2/core/replay-privacy-nodes.js.map +1 -0
  17. package/dist/creator2/core/replay.d.ts +5 -3
  18. package/dist/creator2/core/replay.d.ts.map +1 -1
  19. package/dist/creator2/core/replay.js +74 -41
  20. package/dist/creator2/core/replay.js.map +1 -1
  21. package/dist/creator2/core/transport.d.ts +1 -0
  22. package/dist/creator2/core/transport.d.ts.map +1 -1
  23. package/dist/creator2/core/transport.js.map +1 -1
  24. package/dist/creator2/core/version.d.ts +1 -1
  25. package/dist/creator2/core/version.js +1 -1
  26. package/dist/creator2/creator2/capture.d.ts +1 -1
  27. package/dist/creator2/creator2/capture.d.ts.map +1 -1
  28. package/dist/creator2/creator2/capture.js +9 -16
  29. package/dist/creator2/creator2/capture.js.map +1 -1
  30. package/dist/creator2/creator2/transport.d.ts +1 -0
  31. package/dist/creator2/creator2/transport.d.ts.map +1 -1
  32. package/dist/creator2/creator2/transport.js +4 -0
  33. package/dist/creator2/creator2/transport.js.map +1 -1
  34. package/dist/creator3/core/replay-async-transport.d.ts +4 -0
  35. package/dist/creator3/core/replay-async-transport.d.ts.map +1 -0
  36. package/dist/creator3/core/replay-async-transport.js +16 -0
  37. package/dist/creator3/core/replay-async-transport.js.map +1 -0
  38. package/dist/creator3/core/replay-file.d.ts +5 -0
  39. package/dist/creator3/core/replay-file.d.ts.map +1 -0
  40. package/dist/creator3/core/replay-file.js +31 -0
  41. package/dist/creator3/core/replay-file.js.map +1 -0
  42. package/dist/creator3/core/replay-privacy-nodes.d.ts +14 -0
  43. package/dist/creator3/core/replay-privacy-nodes.d.ts.map +1 -0
  44. package/dist/creator3/core/replay-privacy-nodes.js +15 -0
  45. package/dist/creator3/core/replay-privacy-nodes.js.map +1 -0
  46. package/dist/creator3/core/replay.d.ts +5 -3
  47. package/dist/creator3/core/replay.d.ts.map +1 -1
  48. package/dist/creator3/core/replay.js +23 -9
  49. package/dist/creator3/core/replay.js.map +1 -1
  50. package/dist/creator3/core/transport.d.ts +1 -0
  51. package/dist/creator3/core/transport.d.ts.map +1 -1
  52. package/dist/creator3/core/transport.js.map +1 -1
  53. package/dist/creator3/core/version.d.ts +1 -1
  54. package/dist/creator3/core/version.js +1 -1
  55. package/dist/creator3/creator3/capture.d.ts +1 -1
  56. package/dist/creator3/creator3/capture.d.ts.map +1 -1
  57. package/dist/creator3/creator3/capture.js +6 -13
  58. package/dist/creator3/creator3/capture.js.map +1 -1
  59. package/dist/creator3/creator3/transport.d.ts +1 -0
  60. package/dist/creator3/creator3/transport.d.ts.map +1 -1
  61. package/dist/creator3/creator3/transport.js +4 -0
  62. package/dist/creator3/creator3/transport.js.map +1 -1
  63. package/extensions/creator2/install-native.cjs +126 -6
  64. package/extensions/creator2/install-spm.cjs +190 -0
  65. package/extensions/creator3/install-native.cjs +126 -6
  66. package/extensions/creator3/install-spm.cjs +190 -0
  67. package/native/android/src/main/java/com/ft/sdk/cocos/FTCocosBridge.java +4 -0
  68. package/native/android/src/main/java/com/ft/sdk/cocos/FTCocosReplayImageJobs.java +55 -0
  69. package/native/ios/FTCocosBridge.m +26 -2
  70. package/native/ios/FTCocosBridge.podspec +4 -4
  71. package/native/ios/FTCocosReplayImageJobs.h +11 -0
  72. package/native/ios/FTCocosReplayImageJobs.m +54 -0
  73. package/native/ios/Package.swift +26 -0
  74. package/native/ios/include/FTCocosBridge.h +2 -0
  75. package/native/shared/FTReplayFileBridge.h +62 -0
  76. package/native/shared/FTReplayFileWorker.h +88 -0
  77. package/package.json +2 -1
@@ -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}`);
@@ -30,6 +31,11 @@ function installNative(buildRoot, extensionRoot, logger = console) {
30
31
  const buildNative = path.join(buildRoot, 'cocos-sdk-native');
31
32
  copyDirectory(nativeSource, buildNative);
32
33
  const files = findFiles(buildRoot, 7);
34
+ const replayHeader = path.join(buildNative, 'shared', 'FTReplayFileBridge.h');
35
+ if (fs.existsSync(replayHeader)) {
36
+ findReplayEntryFiles(files)
37
+ .forEach((file) => patchReplayFileBridge(file, replayHeader));
38
+ }
33
39
  const gradlePropertiesFiles = files.filter((file) => path.basename(file) === 'gradle.properties');
34
40
  const gradleFiles = uniqueFiles([
35
41
  ...files.filter((file) => /(?:^|\/)app\/build\.gradle$/.test(normalize(file))),
@@ -42,7 +48,11 @@ function installNative(buildRoot, extensionRoot, logger = console) {
42
48
  const iosApplicationProjects = xcodeProjects
43
49
  .map((projectFile) => ({ projectFile, target: findIosApplicationTarget(projectFile) }))
44
50
  .filter((project) => project.target);
45
- 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) {
46
56
  iosApplicationProjects.forEach(({ projectFile, target }) => {
47
57
  const projectDirectory = path.dirname(path.dirname(projectFile));
48
58
  const podfile = path.join(projectDirectory, 'Podfile');
@@ -52,15 +62,67 @@ function installNative(buildRoot, extensionRoot, logger = console) {
52
62
  }
53
63
  iosApplicationProjects.forEach(({ projectFile, target }) => {
54
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
+ }
55
70
  });
56
71
  gradleFiles.forEach((file) => patchGradle(file, buildNative));
57
72
  gradlePropertiesFiles.forEach(patchGradleProperties);
58
- podfiles.forEach((file) => patchPodfile(file, buildNative));
73
+ if (dependencyManager === 'cocoapods') podfiles.forEach((file) => patchPodfile(file, buildNative));
59
74
  logger.info(
60
- `[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}).`,
61
76
  );
62
77
  }
63
78
 
79
+ function patchReplayFileBridge(file, header) {
80
+ const original = fs.readFileSync(file, 'utf8');
81
+ const includeBegin = '// COCOS_SDK_REPLAY_INCLUDE_BEGIN';
82
+ const includeEnd = '// COCOS_SDK_REPLAY_INCLUDE_END';
83
+ const initBegin = '// COCOS_SDK_REPLAY_INIT_BEGIN';
84
+ const initEnd = '// COCOS_SDK_REPLAY_INIT_END';
85
+ // Creator 3 shares Game.cpp across iOS/Android builds. Keep its include stable
86
+ // when another platform's build directory is cleaned or regenerated.
87
+ const sharedDirectory = path.join(path.dirname(file), 'cocos-sdk-replay');
88
+ const include = `${includeBegin}\n#include "cocos-sdk-replay/FTReplayFileBridge.h"\n${includeEnd}`;
89
+ const init = `${initBegin}\n ft_cocos::installReplayFileBridge();\n ${initEnd}\n `;
90
+ let next = original;
91
+ if (markedPattern(initBegin, initEnd).test(next)) {
92
+ next = next.replace(markedPattern(initBegin, initEnd), init.trimEnd());
93
+ } else {
94
+ const anchor = path.basename(file) === 'Game.cpp'
95
+ ? /\b(?:return\s+)?BaseGame::init\(\);/
96
+ : /\b(?:return\s+)?se->start\(\);/;
97
+ if (!anchor.test(next)) return;
98
+ next = next.replace(anchor, (value) => init + value);
99
+ }
100
+ next = markedPattern(includeBegin, includeEnd).test(next)
101
+ ? next.replace(markedPattern(includeBegin, includeEnd), include)
102
+ : `${include}\n${next}`;
103
+ copyDirectory(path.dirname(header), sharedDirectory);
104
+ if (next !== original) fs.writeFileSync(file, next);
105
+ }
106
+
107
+ function findReplayEntryFiles(files) {
108
+ const entries = files.filter((file) => ['AppDelegate.cpp', 'Game.cpp'].includes(path.basename(file)));
109
+ for (const file of files) {
110
+ let nativeDirectory;
111
+ if (path.basename(file) === 'gradle.properties') {
112
+ const value = readProperty(fs.readFileSync(file, 'utf8'), 'NATIVE_DIR');
113
+ if (value) nativeDirectory = path.resolve(path.dirname(file), value);
114
+ } else if (path.basename(file) === 'CMakeCache.txt') {
115
+ const match = fs.readFileSync(file, 'utf8').match(/^CMAKE_HOME_DIRECTORY:INTERNAL=(.+)$/m);
116
+ if (match) nativeDirectory = match[1].trim();
117
+ }
118
+ if (nativeDirectory) {
119
+ const game = path.resolve(nativeDirectory, '..', 'common', 'Classes', 'Game.cpp');
120
+ if (fs.existsSync(game)) entries.push(game);
121
+ }
122
+ }
123
+ return uniqueFiles(entries);
124
+ }
125
+
64
126
  function patchCocos2IosConfiguration(projectFile, target) {
65
127
  const projectDirectory = path.dirname(path.dirname(projectFile));
66
128
  const iosDirectory = path.join(projectDirectory, 'ios');
@@ -200,9 +262,9 @@ function patchGradle(file, nativeRoot) {
200
262
  " maven { url 'https://mvnrepo.guance.com/repository/maven-releases' }",
201
263
  '}',
202
264
  'dependencies {',
203
- " 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'",
204
266
  " implementation 'com.cloudcare.ft.mobile.sdk.tracker.agent:ft-native:1.1.3'",
205
- " implementation 'com.cloudcare.ft.mobile.sdk.tracker.agent:ft-session-replay:0.1.9-alpha02'",
267
+ " implementation 'com.cloudcare.ft.mobile.sdk.tracker.agent:ft-session-replay:0.1.9-alpha03'",
206
268
  " implementation 'com.google.code.gson:gson:2.10.1'",
207
269
  " implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.22')",
208
270
  " implementation 'androidx.appcompat:appcompat:1.1.0'",
@@ -246,6 +308,10 @@ function replaceIndentedMarkedBlock(contents, begin, end, block) {
246
308
 
247
309
  function findIosApplicationTarget(projectFile) {
248
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
+ }
249
315
  const nativeTarget = /\/\* ([^*]+) \*\/ = \{\s*isa = PBXNativeTarget;([\s\S]*?)\n\s*\};/g;
250
316
  const targets = [];
251
317
  let match;
@@ -330,4 +396,58 @@ function escapeGroovy(value) { return value.replace(/\\/g, '\\\\').replace(/'/g,
330
396
  function escapeRuby(value) { return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); }
331
397
  function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
332
398
 
333
- 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
+ }
@@ -124,6 +124,10 @@ public final class FTCocosBridge {
124
124
 
125
125
  private static Object dispatch(String method, JSONObject payload) throws Exception {
126
126
  switch (method) {
127
+ case "replay.beginSaveImage":
128
+ return FTCocosReplayImageJobs.begin(payload);
129
+ case "replay.pollSaveImage":
130
+ return FTCocosReplayImageJobs.poll(payload.getString("job"));
127
131
  case "hybrid.attach":
128
132
  attachHybrid(payload);
129
133
  return null;
@@ -0,0 +1,55 @@
1
+ package com.ft.sdk.cocos;
2
+
3
+ import org.json.JSONObject;
4
+ import java.util.UUID;
5
+ import java.util.concurrent.ExecutorService;
6
+ import java.util.concurrent.Executors;
7
+
8
+ /** One image in flight across JS runtimes; the caller polls without waiting on disk/encoding. */
9
+ final class FTCocosReplayImageJobs {
10
+ private static final ExecutorService worker = Executors.newSingleThreadExecutor(runnable -> {
11
+ Thread thread = new Thread(runnable, "cocos-replay-image");
12
+ thread.setDaemon(true);
13
+ return thread;
14
+ });
15
+ private static Job current;
16
+
17
+ private static final class Job {
18
+ final String id = UUID.randomUUID().toString();
19
+ String response;
20
+ }
21
+
22
+ static synchronized String begin(JSONObject arguments) throws Exception {
23
+ final String method = arguments.getString("method");
24
+ if (!"replay.saveImage".equals(method) && !"replay.saveImageV2".equals(method)) {
25
+ throw new IllegalArgumentException("Unsupported asynchronous Replay method");
26
+ }
27
+ final String payload = arguments.getJSONObject("arguments").toString();
28
+ if (current != null && current.response == null) throw new IllegalStateException("Replay encoder is busy");
29
+ final Job job = new Job();
30
+ current = job;
31
+ try {
32
+ worker.execute(() -> {
33
+ String response = FTCocosBridge.invokeLocal(method, payload);
34
+ synchronized (FTCocosReplayImageJobs.class) {
35
+ job.response = response;
36
+ }
37
+ });
38
+ } catch (RuntimeException error) {
39
+ current = null;
40
+ throw error;
41
+ }
42
+ return job.id;
43
+ }
44
+
45
+ static synchronized JSONObject poll(String id) throws Exception {
46
+ if (current == null || !current.id.equals(id)) throw new IllegalArgumentException("Replay encoding job is unavailable");
47
+ JSONObject result = new JSONObject();
48
+ result.put("pending", current.response == null);
49
+ if (current.response != null) {
50
+ result.put("response", current.response);
51
+ current = null;
52
+ }
53
+ return result;
54
+ }
55
+ }
@@ -1,6 +1,14 @@
1
1
  #import "FTCocosBridge.h"
2
-
3
- #import "FTMobileSDK.h"
2
+ #import "FTCocosReplayImageJobs.h"
3
+
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"
4
12
  #import "FTSessionReplay.h"
5
13
  #import <UIKit/UIKit.h>
6
14
  #import <objc/message.h>
@@ -24,6 +32,10 @@
24
32
  }
25
33
 
26
34
  + (id)dispatch:(NSString *)method arguments:(NSDictionary *)arguments {
35
+ if ([method isEqualToString:@"replay.beginSaveImage"]) return [self beginSaveImage:arguments];
36
+ if ([method isEqualToString:@"replay.pollSaveImage"]) {
37
+ return [FTCocosReplayImageJobs poll:[self requiredText:arguments key:@"job"]];
38
+ }
27
39
  FTExternalDataManager *rum = [FTExternalDataManager sharedManager];
28
40
  if ([method isEqualToString:@"hybrid.attach"]) {
29
41
  [self ensureNativeHostInitialized];
@@ -254,6 +266,18 @@
254
266
  return nil;
255
267
  }
256
268
 
269
+ + (NSString *)beginSaveImage:(NSDictionary *)arguments {
270
+ NSString *method = [self requiredText:arguments key:@"method"];
271
+ if (![method isEqualToString:@"replay.saveImage"] && ![method isEqualToString:@"replay.saveImageV2"]) {
272
+ [self fail:@"Unsupported asynchronous Replay method"];
273
+ }
274
+ NSDictionary *payload = [[self dictionary:arguments[@"arguments"]] copy];
275
+ if (!payload) [self fail:@"Replay image arguments are required"];
276
+ return [FTCocosReplayImageJobs beginWithWork:^NSString *{
277
+ return [self responseWithValue:[self dispatch:method arguments:payload] error:nil];
278
+ }];
279
+ }
280
+
257
281
  + (void)ensureNativeHostInitialized {
258
282
  @try {
259
283
  if (![FTMobileAgent sharedInstance]) {
@@ -1,18 +1,18 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'FTCocosBridge'
3
- s.version = '0.1.0-alpha.3'
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' }
7
7
  s.author = { 'Guance Cloud' => 'support@guance.com' }
8
8
  s.source = { :path => '.' }
9
9
  s.platform = :ios, '12.0'
10
- s.source_files = 'FTCocosBridge.{h,m}'
10
+ s.source_files = 'FTCocosBridge.{h,m}', 'FTCocosReplayImageJobs.{h,m}'
11
11
  s.public_header_files = 'FTCocosBridge.h'
12
12
  s.requires_arc = true
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,11 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ /** One background image operation, with nonblocking result retrieval from JSB. */
6
+ @interface FTCocosReplayImageJobs : NSObject
7
+ + (NSString *)beginWithWork:(NSString *(^)(void))work;
8
+ + (NSDictionary *)poll:(NSString *)identifier;
9
+ @end
10
+
11
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,54 @@
1
+ #import "FTCocosReplayImageJobs.h"
2
+
3
+ @interface FTCocosReplayImageJob : NSObject
4
+ @property (nonatomic, copy) NSString *identifier;
5
+ @property (nonatomic, copy) NSString *response;
6
+ @end
7
+ @implementation FTCocosReplayImageJob
8
+ @end
9
+
10
+ static FTCocosReplayImageJob *currentJob;
11
+
12
+ @implementation FTCocosReplayImageJobs
13
+
14
+ + (NSString *)beginWithWork:(NSString *(^)(void))work {
15
+ FTCocosReplayImageJob *job = [FTCocosReplayImageJob new];
16
+ job.identifier = NSUUID.UUID.UUIDString;
17
+ @synchronized (self) {
18
+ if (currentJob && !currentJob.response) {
19
+ [NSException raise:NSInternalInconsistencyException format:@"Replay encoder is busy"];
20
+ }
21
+ currentJob = job;
22
+ }
23
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
24
+ @autoreleasepool {
25
+ NSString *response;
26
+ @try {
27
+ response = work();
28
+ } @catch (NSException *exception) {
29
+ NSData *data = [NSJSONSerialization dataWithJSONObject:@{
30
+ @"ok": @NO, @"error": exception.reason ?: @"Replay encoding failed"
31
+ } options:0 error:nil];
32
+ response = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
33
+ }
34
+ @synchronized (self) {
35
+ job.response = response ?: @"{\"ok\":false,\"error\":\"Empty Replay encoding result\"}";
36
+ }
37
+ }
38
+ });
39
+ return job.identifier;
40
+ }
41
+
42
+ + (NSDictionary *)poll:(NSString *)identifier {
43
+ @synchronized (self) {
44
+ if (![currentJob.identifier isEqualToString:identifier]) {
45
+ [NSException raise:NSInvalidArgumentException format:@"Replay encoding job is unavailable"];
46
+ }
47
+ if (!currentJob.response) return @{ @"pending": @YES };
48
+ NSDictionary *result = @{ @"pending": @NO, @"response": currentJob.response };
49
+ currentJob = nil;
50
+ return result;
51
+ }
52
+ }
53
+
54
+ @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"