@truewatchtech/cocos-sdk 0.1.0-alpha.4 → 0.1.0-alpha.7

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 (181) hide show
  1. package/README.md +49 -1
  2. package/bin/cli.cjs +48 -18
  3. package/bin/legacy-asset-hashes.json +32 -0
  4. package/bin/managed-assets.cjs +67 -0
  5. package/dist/creator2/core/client.d.ts +55 -7
  6. package/dist/creator2/core/client.d.ts.map +1 -1
  7. package/dist/creator2/core/client.js +74 -28
  8. package/dist/creator2/core/client.js.map +1 -1
  9. package/dist/creator2/core/index.d.ts +1 -1
  10. package/dist/creator2/core/index.d.ts.map +1 -1
  11. package/dist/creator2/core/index.js.map +1 -1
  12. package/dist/creator2/core/plugin-api.d.ts +15 -0
  13. package/dist/creator2/core/plugin-api.d.ts.map +1 -0
  14. package/dist/creator2/core/plugin-api.js +3 -0
  15. package/dist/creator2/core/plugin-api.js.map +1 -0
  16. package/dist/creator2/core/transport.d.ts +20 -0
  17. package/dist/creator2/core/transport.d.ts.map +1 -1
  18. package/dist/creator2/core/transport.js.map +1 -1
  19. package/dist/creator2/core/types.d.ts +0 -78
  20. package/dist/creator2/core/types.d.ts.map +1 -1
  21. package/dist/creator2/core/validation.d.ts +0 -11
  22. package/dist/creator2/core/validation.d.ts.map +1 -1
  23. package/dist/creator2/core/validation.js +0 -63
  24. package/dist/creator2/core/validation.js.map +1 -1
  25. package/dist/creator2/core/version.d.ts +1 -1
  26. package/dist/creator2/core/version.js +1 -1
  27. package/dist/creator2/creator2/auto.d.ts +1 -3
  28. package/dist/creator2/creator2/auto.d.ts.map +1 -1
  29. package/dist/creator2/creator2/auto.js +0 -25
  30. package/dist/creator2/creator2/auto.js.map +1 -1
  31. package/dist/creator2/creator2/index.d.ts +0 -7
  32. package/dist/creator2/creator2/index.d.ts.map +1 -1
  33. package/dist/creator2/creator2/index.js +1 -13
  34. package/dist/creator2/creator2/index.js.map +1 -1
  35. package/dist/creator2/creator2/transport.d.ts +0 -1
  36. package/dist/creator2/creator2/transport.d.ts.map +1 -1
  37. package/dist/creator2/creator2/transport.js +0 -4
  38. package/dist/creator2/creator2/transport.js.map +1 -1
  39. package/dist/creator3/core/client.d.ts +55 -7
  40. package/dist/creator3/core/client.d.ts.map +1 -1
  41. package/dist/creator3/core/client.js +72 -28
  42. package/dist/creator3/core/client.js.map +1 -1
  43. package/dist/creator3/core/index.d.ts +1 -1
  44. package/dist/creator3/core/index.d.ts.map +1 -1
  45. package/dist/creator3/core/index.js.map +1 -1
  46. package/dist/creator3/core/plugin-api.d.ts +15 -0
  47. package/dist/creator3/core/plugin-api.d.ts.map +1 -0
  48. package/dist/creator3/core/plugin-api.js +2 -0
  49. package/dist/creator3/core/plugin-api.js.map +1 -0
  50. package/dist/creator3/core/transport.d.ts +20 -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/types.d.ts +0 -78
  54. package/dist/creator3/core/types.d.ts.map +1 -1
  55. package/dist/creator3/core/validation.d.ts +0 -11
  56. package/dist/creator3/core/validation.d.ts.map +1 -1
  57. package/dist/creator3/core/validation.js +0 -61
  58. package/dist/creator3/core/validation.js.map +1 -1
  59. package/dist/creator3/core/version.d.ts +1 -1
  60. package/dist/creator3/core/version.js +1 -1
  61. package/dist/creator3/creator3/auto.d.ts +1 -3
  62. package/dist/creator3/creator3/auto.d.ts.map +1 -1
  63. package/dist/creator3/creator3/auto.js +1 -24
  64. package/dist/creator3/creator3/auto.js.map +1 -1
  65. package/dist/creator3/creator3/index.d.ts +0 -7
  66. package/dist/creator3/creator3/index.d.ts.map +1 -1
  67. package/dist/creator3/creator3/index.js +1 -12
  68. package/dist/creator3/creator3/index.js.map +1 -1
  69. package/dist/creator3/creator3/transport.d.ts +0 -1
  70. package/dist/creator3/creator3/transport.d.ts.map +1 -1
  71. package/dist/creator3/creator3/transport.js +0 -4
  72. package/dist/creator3/creator3/transport.js.map +1 -1
  73. package/extensions/creator2/install-native.cjs +118 -28
  74. package/extensions/creator2/install-spm.cjs +198 -0
  75. package/extensions/creator2/legacy-asset-hashes.json +32 -0
  76. package/extensions/creator2/managed-assets.cjs +67 -0
  77. package/extensions/creator3/install-native.cjs +118 -28
  78. package/extensions/creator3/install-spm.cjs +198 -0
  79. package/extensions/creator3/legacy-asset-hashes.json +32 -0
  80. package/extensions/creator3/managed-assets.cjs +67 -0
  81. package/native/android/src/main/java/com/ft/sdk/cocos/FTBridgeTransport.java +85 -0
  82. package/native/android/src/main/java/com/ft/sdk/cocos/FTCocosBridge.java +2 -192
  83. package/native/android/src/main/java/com/ft/sdk/cocos/FTCocosBridgeModules.java +9 -0
  84. package/native/android/src/main/java/com/ft/sdk/cocos/FTCocosBridgeProvider.java +1 -1
  85. package/native/ios/FTCocosBridge.m +8 -203
  86. package/native/ios/FTCocosBridge.podspec +3 -4
  87. package/native/ios/Package.swift +25 -0
  88. package/native/ios/include/FTCocosBridge.h +2 -0
  89. package/package.json +10 -5
  90. package/plugin-api/package.json +4 -0
  91. package/components/creator2/ReplayPrivacy.ts +0 -23
  92. package/components/creator3/ReplayPrivacy.ts +0 -25
  93. package/dist/creator2/core/replay-async-transport.d.ts +0 -4
  94. package/dist/creator2/core/replay-async-transport.d.ts.map +0 -1
  95. package/dist/creator2/core/replay-async-transport.js +0 -67
  96. package/dist/creator2/core/replay-async-transport.js.map +0 -1
  97. package/dist/creator2/core/replay-budget.d.ts +0 -28
  98. package/dist/creator2/core/replay-budget.d.ts.map +0 -1
  99. package/dist/creator2/core/replay-budget.js +0 -113
  100. package/dist/creator2/core/replay-budget.js.map +0 -1
  101. package/dist/creator2/core/replay-diagnostics.d.ts +0 -36
  102. package/dist/creator2/core/replay-diagnostics.d.ts.map +0 -1
  103. package/dist/creator2/core/replay-diagnostics.js +0 -40
  104. package/dist/creator2/core/replay-diagnostics.js.map +0 -1
  105. package/dist/creator2/core/replay-encoding.d.ts +0 -31
  106. package/dist/creator2/core/replay-encoding.d.ts.map +0 -1
  107. package/dist/creator2/core/replay-encoding.js +0 -63
  108. package/dist/creator2/core/replay-encoding.js.map +0 -1
  109. package/dist/creator2/core/replay-file.d.ts +0 -5
  110. package/dist/creator2/core/replay-file.d.ts.map +0 -1
  111. package/dist/creator2/core/replay-file.js +0 -101
  112. package/dist/creator2/core/replay-file.js.map +0 -1
  113. package/dist/creator2/core/replay-pixels.d.ts +0 -3
  114. package/dist/creator2/core/replay-pixels.d.ts.map +0 -1
  115. package/dist/creator2/core/replay-pixels.js +0 -16
  116. package/dist/creator2/core/replay-pixels.js.map +0 -1
  117. package/dist/creator2/core/replay-privacy-nodes.d.ts +0 -14
  118. package/dist/creator2/core/replay-privacy-nodes.d.ts.map +0 -1
  119. package/dist/creator2/core/replay-privacy-nodes.js +0 -18
  120. package/dist/creator2/core/replay-privacy-nodes.js.map +0 -1
  121. package/dist/creator2/core/replay-privacy.d.ts +0 -15
  122. package/dist/creator2/core/replay-privacy.d.ts.map +0 -1
  123. package/dist/creator2/core/replay-privacy.js +0 -36
  124. package/dist/creator2/core/replay-privacy.js.map +0 -1
  125. package/dist/creator2/core/replay.d.ts +0 -99
  126. package/dist/creator2/core/replay.d.ts.map +0 -1
  127. package/dist/creator2/core/replay.js +0 -611
  128. package/dist/creator2/core/replay.js.map +0 -1
  129. package/dist/creator2/creator2/capture.d.ts +0 -17
  130. package/dist/creator2/creator2/capture.d.ts.map +0 -1
  131. package/dist/creator2/creator2/capture.js +0 -168
  132. package/dist/creator2/creator2/capture.js.map +0 -1
  133. package/dist/creator3/core/replay-async-transport.d.ts +0 -4
  134. package/dist/creator3/core/replay-async-transport.d.ts.map +0 -1
  135. package/dist/creator3/core/replay-async-transport.js +0 -16
  136. package/dist/creator3/core/replay-async-transport.js.map +0 -1
  137. package/dist/creator3/core/replay-budget.d.ts +0 -28
  138. package/dist/creator3/core/replay-budget.d.ts.map +0 -1
  139. package/dist/creator3/core/replay-budget.js +0 -105
  140. package/dist/creator3/core/replay-budget.js.map +0 -1
  141. package/dist/creator3/core/replay-diagnostics.d.ts +0 -36
  142. package/dist/creator3/core/replay-diagnostics.d.ts.map +0 -1
  143. package/dist/creator3/core/replay-diagnostics.js +0 -36
  144. package/dist/creator3/core/replay-diagnostics.js.map +0 -1
  145. package/dist/creator3/core/replay-encoding.d.ts +0 -31
  146. package/dist/creator3/core/replay-encoding.d.ts.map +0 -1
  147. package/dist/creator3/core/replay-encoding.js +0 -62
  148. package/dist/creator3/core/replay-encoding.js.map +0 -1
  149. package/dist/creator3/core/replay-file.d.ts +0 -5
  150. package/dist/creator3/core/replay-file.d.ts.map +0 -1
  151. package/dist/creator3/core/replay-file.js +0 -31
  152. package/dist/creator3/core/replay-file.js.map +0 -1
  153. package/dist/creator3/core/replay-pixels.d.ts +0 -3
  154. package/dist/creator3/core/replay-pixels.d.ts.map +0 -1
  155. package/dist/creator3/core/replay-pixels.js +0 -13
  156. package/dist/creator3/core/replay-pixels.js.map +0 -1
  157. package/dist/creator3/core/replay-privacy-nodes.d.ts +0 -14
  158. package/dist/creator3/core/replay-privacy-nodes.d.ts.map +0 -1
  159. package/dist/creator3/core/replay-privacy-nodes.js +0 -15
  160. package/dist/creator3/core/replay-privacy-nodes.js.map +0 -1
  161. package/dist/creator3/core/replay-privacy.d.ts +0 -15
  162. package/dist/creator3/core/replay-privacy.d.ts.map +0 -1
  163. package/dist/creator3/core/replay-privacy.js +0 -33
  164. package/dist/creator3/core/replay-privacy.js.map +0 -1
  165. package/dist/creator3/core/replay.d.ts +0 -99
  166. package/dist/creator3/core/replay.d.ts.map +0 -1
  167. package/dist/creator3/core/replay.js +0 -534
  168. package/dist/creator3/core/replay.js.map +0 -1
  169. package/dist/creator3/creator3/capture.d.ts +0 -17
  170. package/dist/creator3/creator3/capture.d.ts.map +0 -1
  171. package/dist/creator3/creator3/capture.js +0 -112
  172. package/dist/creator3/creator3/capture.js.map +0 -1
  173. package/dist/creator3/creator3/replay-render-cycle.d.ts +0 -9
  174. package/dist/creator3/creator3/replay-render-cycle.d.ts.map +0 -1
  175. package/dist/creator3/creator3/replay-render-cycle.js +0 -12
  176. package/dist/creator3/creator3/replay-render-cycle.js.map +0 -1
  177. package/native/android/src/main/java/com/ft/sdk/cocos/FTCocosReplayImageJobs.java +0 -55
  178. package/native/ios/FTCocosReplayImageJobs.h +0 -11
  179. package/native/ios/FTCocosReplayImageJobs.m +0 -54
  180. package/native/shared/FTReplayFileBridge.h +0 -62
  181. package/native/shared/FTReplayFileWorker.h +0 -88
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { createHash } = require('crypto');
5
+ const legacyHashes = new Set(require('./legacy-asset-hashes.json'));
6
+ // Older combined installations predate the ownership manifest. Never leave a
7
+ // locally modified obsolete Replay bridge compiling beside the new bridge.
8
+ const legacyReplayNames = new Set(['FTCocosReplayImageJobs.java', 'FTCocosReplayImageJobs.h', 'FTCocosReplayImageJobs.m', 'FTReplayFileBridge.h', 'FTReplayFileWorker.h']);
9
+ const manifestName = '.cocos-sdk-managed.json';
10
+ const hash = bytes => createHash('sha256').update(bytes).digest('hex');
11
+
12
+ function files(root) {
13
+ const result = [];
14
+ if (!fs.existsSync(root)) return result;
15
+ function visit(directory) {
16
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
17
+ if (entry.name === manifestName || entry.name === '.DS_Store') continue;
18
+ const file = path.join(directory, entry.name);
19
+ if (entry.isSymbolicLink()) throw new Error(`[cocos-sdk] Refusing to copy symbolic link: ${file}`);
20
+ if (entry.isDirectory()) visit(file);
21
+ else result.push(path.relative(root, file).split(path.sep).join('/'));
22
+ }
23
+ }
24
+ visit(root);
25
+ return result;
26
+ }
27
+
28
+ /** Reconciles only files written by the SDK, preserving unrelated project assets. */
29
+ function syncManagedDirectory(destination, sources, overrides = {}) {
30
+ const desired = new Map();
31
+ for (const { root, prefix = '' } of sources) {
32
+ if (!fs.existsSync(root)) throw new Error(`[cocos-sdk] Missing package assets: ${root}`);
33
+ for (const file of files(root)) desired.set(prefix + file, fs.readFileSync(path.join(root, file)));
34
+ }
35
+ for (const [file, content] of Object.entries(overrides)) desired.set(file, Buffer.from(content));
36
+ const manifest = path.join(destination, manifestName);
37
+ const previous = fs.existsSync(manifest) ? JSON.parse(fs.readFileSync(manifest, 'utf8')) : {};
38
+ const existing = files(destination);
39
+ const remove = [];
40
+ // Check every conflict before writing or deleting any managed file.
41
+ for (const file of existing) {
42
+ const digest = hash(fs.readFileSync(path.join(destination, file)));
43
+ const old = previous[file];
44
+ const next = desired.get(file);
45
+ const legacy = legacyHashes.has(digest);
46
+ if (!next && !old && !legacy && legacyReplayNames.has(path.basename(file))) {
47
+ throw new Error(`[cocos-sdk] Preserve or reconcile modified legacy Replay asset before reinstalling: ${path.join(destination, file)}`);
48
+ }
49
+ if ((old || next) && digest !== old && !legacy && (!next || digest !== hash(next))) {
50
+ throw new Error(`[cocos-sdk] Preserve or reconcile modified SDK asset before reinstalling: ${path.join(destination, file)}`);
51
+ }
52
+ if (!next && (old || legacy) && !file.endsWith('.meta')) remove.push(file);
53
+ }
54
+ fs.mkdirSync(destination, { recursive: true });
55
+ for (const file of remove) fs.unlinkSync(path.join(destination, file));
56
+ const nextManifest = {};
57
+ for (const [file, content] of desired) {
58
+ const target = path.resolve(destination, file);
59
+ if (!target.startsWith(path.resolve(destination) + path.sep)) throw new Error('Invalid SDK asset path');
60
+ fs.mkdirSync(path.dirname(target), { recursive: true });
61
+ if (!fs.existsSync(target) || hash(fs.readFileSync(target)) !== hash(content)) fs.writeFileSync(target, content);
62
+ nextManifest[file] = hash(content);
63
+ }
64
+ fs.writeFileSync(manifest, `${JSON.stringify(nextManifest, null, 2)}\n`);
65
+ }
66
+
67
+ module.exports = { syncManagedDirectory };
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { execFileSync } = require('child_process');
6
+ const { syncManagedDirectory } = require('./managed-assets.cjs');
6
7
 
7
8
  const BEGIN = '/* COCOS_SDK_BEGIN */';
8
9
  const END = '/* COCOS_SDK_END */';
@@ -22,19 +23,38 @@ function installNative(buildRoot, extensionRoot, logger = console) {
22
23
  buildRoot = path.resolve(buildRoot);
23
24
  extensionRoot = path.resolve(extensionRoot);
24
25
  if (!fs.existsSync(buildRoot)) return;
26
+ const dependencyManager = readIosDependencyManager(buildRoot, extensionRoot);
25
27
  const nativeSource = path.join(extensionRoot, 'native');
26
28
  if (!fs.existsSync(nativeSource)) {
27
29
  logger.warn(`[cocos-sdk] Native bridge not found at ${nativeSource}`);
28
30
  return;
29
31
  }
32
+ const replayDescriptor = path.join(extensionRoot, 'replay-integration.json');
33
+ const replay = fs.existsSync(replayDescriptor) ? JSON.parse(fs.readFileSync(replayDescriptor, 'utf8')) : null;
34
+ const installedFile = path.join(extensionRoot, 'sdk-integration.json');
35
+ const installed = fs.existsSync(installedFile) ? JSON.parse(fs.readFileSync(installedFile, 'utf8')) : null;
36
+ if (installed?.replay && !replay) throw new Error('[cocos-sdk] Replay assets are missing. Run truewatch-cocos install --replay.');
37
+ if (replay && (replay.schemaVersion !== 1 || !installed || replay.baseVersion !== installed.version || replay.version !== installed.version || replay.iosSdkVersion !== '1.6.8-alpha.5')) {
38
+ throw new Error('[cocos-sdk] Replay native integration version does not match the base SDK. Reinstall matching packages.');
39
+ }
40
+ const replaySource = path.join(extensionRoot, 'replay-native');
30
41
  const buildNative = path.join(buildRoot, 'cocos-sdk-native');
31
- copyDirectory(nativeSource, buildNative);
32
- const files = findFiles(buildRoot, 7);
33
- const replayHeader = path.join(buildNative, 'shared', 'FTReplayFileBridge.h');
34
- if (fs.existsSync(replayHeader)) {
35
- findReplayEntryFiles(files)
36
- .forEach((file) => patchReplayFileBridge(file, replayHeader));
42
+ const sources = [{ root: nativeSource }];
43
+ if (replay) sources.push({ root: replaySource, prefix: 'replay/' });
44
+ if (dependencyManager === 'spm') {
45
+ sources.push({ root: path.join(nativeSource, 'ios'), prefix: 'FTCocosBridge/' });
46
+ if (replay) sources.push({ root: path.join(replaySource, 'ios'), prefix: 'FTCocosReplayBridge/' });
37
47
  }
48
+ const modules = 'android/src/main/java/com/ft/sdk/cocos/FTCocosBridgeModules.java';
49
+ const dispatcher = fs.readFileSync(path.join(nativeSource, modules), 'utf8');
50
+ const configuredDispatcher = replay ? dispatcher.replace(
51
+ ' return "{',
52
+ ' if ("session-replay".equals(endpoint)) return FTCocosReplayBridge.invokeLocal(method, payload);\n return "{',
53
+ ) : dispatcher;
54
+ syncManagedDirectory(buildNative, sources, { [modules]: configuredDispatcher });
55
+ const files = findFiles(buildRoot, 7);
56
+ const replayHeader = replay ? path.join(buildNative, 'replay', 'shared', 'FTReplayFileBridge.h') : null;
57
+ findReplayEntryFiles(files).forEach(file => patchReplayFileBridge(file, replayHeader));
38
58
  const gradlePropertiesFiles = files.filter((file) => path.basename(file) === 'gradle.properties');
39
59
  const gradleFiles = uniqueFiles([
40
60
  ...files.filter((file) => /(?:^|\/)app\/build\.gradle$/.test(normalize(file))),
@@ -47,7 +67,10 @@ function installNative(buildRoot, extensionRoot, logger = console) {
47
67
  const iosApplicationProjects = xcodeProjects
48
68
  .map((projectFile) => ({ projectFile, target: findIosApplicationTarget(projectFile) }))
49
69
  .filter((project) => project.target);
50
- if (podfiles.length === 0) {
70
+ if (dependencyManager === 'spm' && iosApplicationProjects.length > 0) {
71
+ migrateManagedPods(podfiles, logger);
72
+ }
73
+ if (dependencyManager === 'cocoapods' && podfiles.length === 0) {
51
74
  iosApplicationProjects.forEach(({ projectFile, target }) => {
52
75
  const projectDirectory = path.dirname(path.dirname(projectFile));
53
76
  const podfile = path.join(projectDirectory, 'Podfile');
@@ -57,12 +80,21 @@ function installNative(buildRoot, extensionRoot, logger = console) {
57
80
  }
58
81
  iosApplicationProjects.forEach(({ projectFile, target }) => {
59
82
  patchCocos2IosConfiguration(projectFile, target);
83
+ if (dependencyManager === 'spm') {
84
+ const spm = require('./install-spm.cjs');
85
+ if (!replay) spm.removeSwiftPackages(projectFile, ['FTCocosReplayBridge']);
86
+ spm.installSwiftPackage(projectFile, path.join(buildNative, 'FTCocosBridge'), 'FTCocosBridge', target);
87
+ if (replay) spm.installSwiftPackage(projectFile, path.join(buildNative, 'FTCocosReplayBridge'), 'FTCocosReplayBridge', target);
88
+ } else if (fs.readFileSync(projectFile, 'utf8').includes('XCLocalSwiftPackageReference')
89
+ || fs.existsSync(path.join(path.dirname(projectFile), 'cocos-sdk-spm.json'))) {
90
+ require('./install-spm.cjs').removeSwiftPackages(projectFile);
91
+ }
60
92
  });
61
- gradleFiles.forEach((file) => patchGradle(file, buildNative));
93
+ gradleFiles.forEach((file) => patchGradle(file, buildNative, replay));
62
94
  gradlePropertiesFiles.forEach(patchGradleProperties);
63
- podfiles.forEach((file) => patchPodfile(file, buildNative));
95
+ if (dependencyManager === 'cocoapods') podfiles.forEach((file) => patchPodfile(file, buildNative, replay));
64
96
  logger.info(
65
- `[cocos-sdk] Installed native bridge (${gradleFiles.length} Android, ${podfiles.length} iOS project files).`,
97
+ `[cocos-sdk] Installed native bridge (${gradleFiles.length} Android, ${dependencyManager === 'spm' ? iosApplicationProjects.length : podfiles.length} iOS project files; iOS: ${dependencyManager}).`,
66
98
  );
67
99
  }
68
100
 
@@ -75,6 +107,12 @@ function patchReplayFileBridge(file, header) {
75
107
  // Creator 3 shares Game.cpp across iOS/Android builds. Keep its include stable
76
108
  // when another platform's build directory is cleaned or regenerated.
77
109
  const sharedDirectory = path.join(path.dirname(file), 'cocos-sdk-replay');
110
+ if (!header) {
111
+ const next = original.replace(markedPattern(includeBegin, includeEnd), '').replace(markedPattern(initBegin, initEnd), '');
112
+ if (fs.existsSync(sharedDirectory)) syncManagedDirectory(sharedDirectory, []);
113
+ if (next !== original) fs.writeFileSync(file, next);
114
+ return;
115
+ }
78
116
  const include = `${includeBegin}\n#include "cocos-sdk-replay/FTReplayFileBridge.h"\n${includeEnd}`;
79
117
  const init = `${initBegin}\n ft_cocos::installReplayFileBridge();\n ${initEnd}\n `;
80
118
  let next = original;
@@ -90,7 +128,7 @@ function patchReplayFileBridge(file, header) {
90
128
  next = markedPattern(includeBegin, includeEnd).test(next)
91
129
  ? next.replace(markedPattern(includeBegin, includeEnd), include)
92
130
  : `${include}\n${next}`;
93
- copyDirectory(path.dirname(header), sharedDirectory);
131
+ syncManagedDirectory(sharedDirectory, [{ root: path.dirname(header) }]);
94
132
  if (next !== original) fs.writeFileSync(file, next);
95
133
  }
96
134
 
@@ -241,34 +279,37 @@ function compareVersions(left, right) {
241
279
  return 0;
242
280
  }
243
281
 
244
- function patchGradle(file, nativeRoot) {
282
+ function patchGradle(file, nativeRoot, replay) {
245
283
  const javaDir = normalize(path.join(nativeRoot, 'android', 'src', 'main', 'java'));
284
+ const javaDirs = [javaDir];
285
+ if (replay) javaDirs.push(normalize(path.join(nativeRoot, 'replay', 'android', 'src', 'main', 'java')));
246
286
  replaceMarkedBlock(file, [
247
287
  BEGIN,
248
288
  'android {',
249
- ` sourceSets { main.java.srcDirs += ['${escapeGroovy(javaDir)}'] }`,
289
+ ` sourceSets { main.java.srcDirs += [${javaDirs.map(dir => `'${escapeGroovy(dir)}'`).join(', ')}] }`,
250
290
  '}',
251
291
  'repositories {',
252
292
  " maven { url 'https://mvnrepo.truewatch.com/repository/maven-releases' }",
253
293
  '}',
254
294
  'dependencies {',
255
- " implementation 'com.truewatch.ft.mobile.sdk.tracker.agent:ft-sdk:1.7.6-alpha02'",
295
+ " implementation 'com.truewatch.ft.mobile.sdk.tracker.agent:ft-sdk:1.7.6-alpha03'",
256
296
  " implementation 'com.truewatch.ft.mobile.sdk.tracker.agent:ft-native:1.1.3'",
257
- " implementation 'com.truewatch.ft.mobile.sdk.tracker.agent:ft-session-replay:0.1.9-alpha03'",
297
+ ...(replay ? [` implementation '${escapeGroovy(replay.androidReplay)}'`] : []),
258
298
  " implementation 'com.google.code.gson:gson:2.10.1'",
259
299
  " implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.22')",
260
300
  " implementation 'androidx.appcompat:appcompat:1.1.0'",
261
- " implementation 'androidx.fragment:fragment:1.8.0'",
301
+ ...(replay ? [` implementation '${escapeGroovy(replay.androidFragment)}'`] : []),
262
302
  '}',
263
303
  END,
264
304
  ].join('\n'), BEGIN, END, [[LEGACY_BEGIN, LEGACY_END]]);
265
305
  }
266
306
 
267
- function patchPodfile(file, nativeRoot) {
307
+ function patchPodfile(file, nativeRoot, replay) {
268
308
  const iosDir = normalize(path.join(nativeRoot, 'ios'));
269
309
  const block = [
270
310
  POD_BEGIN,
271
311
  `pod 'FTCocosBridge', :path => '${escapeRuby(iosDir)}'`,
312
+ ...(replay ? [`pod 'FTCocosReplayBridge', :path => '${escapeRuby(normalize(path.join(nativeRoot, 'replay', 'ios')))}'`] : []),
272
313
  POD_END,
273
314
  ].join('\n');
274
315
  const original = fs.readFileSync(file, 'utf8');
@@ -298,6 +339,11 @@ function replaceIndentedMarkedBlock(contents, begin, end, block) {
298
339
 
299
340
  function findIosApplicationTarget(projectFile) {
300
341
  const contents = fs.readFileSync(projectFile, 'utf8');
342
+ if (contents.includes('COCOS_SDK_XCODE_PROJECT')
343
+ || fs.existsSync(path.join(path.dirname(projectFile), 'cocos-sdk-spm.json'))) {
344
+ const { readProject, applicationTarget } = require('./install-spm.cjs');
345
+ return applicationTarget(readProject(projectFile))?.[1].name || null;
346
+ }
301
347
  const nativeTarget = /\/\* ([^*]+) \*\/ = \{\s*isa = PBXNativeTarget;([\s\S]*?)\n\s*\};/g;
302
348
  const targets = [];
303
349
  let match;
@@ -352,16 +398,6 @@ function markedPattern(begin, end) {
352
398
  return new RegExp(`${escapeRegExp(begin)}[\\s\\S]*?${escapeRegExp(end)}`, 'm');
353
399
  }
354
400
 
355
- function copyDirectory(source, destination) {
356
- fs.mkdirSync(destination, { recursive: true });
357
- for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
358
- const from = path.join(source, entry.name);
359
- const to = path.join(destination, entry.name);
360
- if (entry.isDirectory()) copyDirectory(from, to);
361
- else fs.copyFileSync(from, to);
362
- }
363
- }
364
-
365
401
  function findFiles(root, maxDepth) {
366
402
  const result = [];
367
403
  function visit(directory, depth) {
@@ -382,4 +418,58 @@ function escapeGroovy(value) { return value.replace(/\\/g, '\\\\').replace(/'/g,
382
418
  function escapeRuby(value) { return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); }
383
419
  function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
384
420
 
385
- module.exports = { installNative };
421
+ function readIosDependencyManager(buildRoot, extensionRoot) {
422
+ for (const start of [buildRoot, extensionRoot]) {
423
+ let directory = path.resolve(start);
424
+ while (true) {
425
+ const file = path.join(directory, 'cocos-sdk.config.json');
426
+ if (fs.existsSync(file)) {
427
+ const config = JSON.parse(fs.readFileSync(file, 'utf8'));
428
+ const manager = config.ios?.dependencyManager ?? 'cocoapods';
429
+ if (!['spm', 'cocoapods'].includes(manager)) {
430
+ throw new Error(`[cocos-sdk] Invalid ios.dependencyManager in ${file}: expected spm or cocoapods.`);
431
+ }
432
+ return manager;
433
+ }
434
+ const parent = path.dirname(directory);
435
+ if (parent === directory) break;
436
+ directory = parent;
437
+ }
438
+ }
439
+ return 'cocoapods';
440
+ }
441
+
442
+ function migrateManagedPods(podfiles, logger) {
443
+ for (const file of podfiles) {
444
+ const original = fs.readFileSync(file, 'utf8');
445
+ let next = original;
446
+ for (const [begin, end] of [
447
+ [POD_BEGIN, POD_END], [LEGACY_POD_BEGIN, LEGACY_POD_END], [LEGACY_BEGIN, LEGACY_END],
448
+ ['# COCOS_HYBRID_SAMPLE_BEGIN', '# COCOS_HYBRID_SAMPLE_END'],
449
+ ]) next = next.replace(markedPattern(begin, end), '');
450
+ if (/^\s*pod\s+['"](?:FTCocosBridge|FTCocosReplayBridge|HybridSampleHost|TrueWatchSDK)(?:\/[^'"]*)?['"]/m.test(next)) {
451
+ throw new Error(`[cocos-sdk] Remove the manually declared native SDK Pod from ${file} before switching to SPM.`);
452
+ }
453
+ if (next !== original) fs.writeFileSync(file, next);
454
+ const directory = path.dirname(file);
455
+ const existingLock = path.join(directory, 'Podfile.lock');
456
+ const hasSDKPods = fs.existsSync(existingLock)
457
+ && /^\s*- (?:FTCocosBridge|FTCocosReplayBridge|HybridSampleHost|TrueWatchSDK)(?:\/|\s|:)/m.test(fs.readFileSync(existingLock, 'utf8'));
458
+ if (next === original && !hasSDKPods) continue;
459
+ if (fs.existsSync(path.join(directory, 'Podfile.lock')) || fs.existsSync(path.join(directory, 'Pods'))) {
460
+ logger.info('[cocos-sdk] Updating existing Pods integration for the SPM switch.');
461
+ try {
462
+ execFileSync('pod', ['install'], { cwd: directory, encoding: 'utf8', maxBuffer: 4 * 1024 * 1024 });
463
+ } catch (error) {
464
+ fs.writeFileSync(file, original);
465
+ throw new Error(`[cocos-sdk] Unable to update existing Pods in ${directory}: ${error.message}`);
466
+ }
467
+ const lock = path.join(directory, 'Podfile.lock');
468
+ if (fs.existsSync(lock) && /^\s*- TrueWatchSDK(?:\/|\s|:)/m.test(fs.readFileSync(lock, 'utf8'))) {
469
+ throw new Error(`[cocos-sdk] Another Pod still depends on the native SDK in ${lock}; migrate that dependency before using SPM.`);
470
+ }
471
+ }
472
+ }
473
+ }
474
+
475
+ module.exports = { installNative, readIosDependencyManager };
@@ -0,0 +1,198 @@
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, productNames = ['FTCocosBridge', 'FTCocosReplayBridge', 'HybridSampleHost']) {
149
+ const project = readProject(projectFile);
150
+ const objects = project.objects;
151
+ const removed = new Set();
152
+ for (const productName of productNames) {
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
+ for (const id of removed) delete objects[id];
163
+ for (const object of Object.values(objects)) {
164
+ for (const key of ['packageReferences', 'packageProductDependencies', 'files']) {
165
+ if (Array.isArray(object[key])) object[key] = object[key].filter((id) => !removed.has(id));
166
+ }
167
+
168
+ }
169
+ const stateFile = path.join(path.dirname(projectFile), 'cocos-sdk-spm.json');
170
+ if (fs.existsSync(stateFile)) {
171
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
172
+ state.packages = state.packages.filter(item => !productNames.includes(item.productName));
173
+ if (state.packages.length) {
174
+ fs.writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\n`);
175
+ } else {
176
+ for (const [id, settings] of Object.entries(state.buildLocations)) {
177
+ if (objects[id]) Object.assign(objects[id].buildSettings ||= {}, settings);
178
+ }
179
+ for (const object of Object.values(objects)) {
180
+ if (object.isa === 'PBXShellScriptBuildPhase') object.shellScript = (object.shellScript || '').replace(restorePattern, '');
181
+ }
182
+ fs.unlinkSync(stateFile);
183
+ fs.rmSync(path.join(path.dirname(projectFile), 'cocos-sdk-spm.cjs'), { force: true });
184
+ }
185
+ }
186
+
187
+ writeProject(projectFile, project);
188
+ }
189
+
190
+ module.exports = { installSwiftPackage, removeSwiftPackages, readProject, applicationTarget };
191
+
192
+ if (require.main === module && process.argv[2] === '--restore') {
193
+ const directory = path.dirname(__filename);
194
+ const state = JSON.parse(fs.readFileSync(path.join(directory, 'cocos-sdk-spm.json'), 'utf8'));
195
+ for (const item of state.packages) {
196
+ installSwiftPackage(path.join(directory, 'project.pbxproj'), path.resolve(directory, item.packageDirectory), item.productName, item.targetName);
197
+ }
198
+ }
@@ -0,0 +1,32 @@
1
+ [
2
+ "058341971ed42ddfde76c28ce19fcc0bba5020fec1f7e1c613c7bda7d2df7677",
3
+ "11ee16e609096ccfe90b17f61d87ab65275a39df4bef6f8c29577e7395aa00f8",
4
+ "15338071a78f1505265cc8efbdc9628e9362f7a2c9fc00989d57461126f8f3f5",
5
+ "1a51af27d68cdf0bf3ce709c9cd60f9b8a2d7defd46d06896ce9a916e1dfc767",
6
+ "2caa0d3d542f5cca0b6b7f3aa2851c7e296b08fce286202a844b52920b1f6dc0",
7
+ "3189144de638b936e14d64ddbf9d4c95e95146ceecc357021473ca8adcc757d1",
8
+ "48ff77a12a7b9b59172f5470a642f23e4c61e9f7e10ee6d9d3f4f41377d696ac",
9
+ "4d2dcd924f1073b8a6fb94d342bee33a281f93c19302104be172e5de9981b3f0",
10
+ "51c71e12df61cfe7e75c3d97080e31a398fbea8719a3e964cec5dd332df1202c",
11
+ "54340b775f6ba897ade2df2d9fcfc39d17372e246e951d77dd166f00265815a8",
12
+ "5f33108f27f38836214f2c11badd7bb76ba9ec090f9d5b1490834a9257976939",
13
+ "71e6ade990a65d1bec51fec02c94bfe804e591d47df7ed9a271c981e9cf1d71c",
14
+ "83df7e51a5e4f76aab6310870c665fc80334808c64fb9c92fee46b466e1b58c3",
15
+ "85ad9622480d954c33a4f9442a391f6d3d9d617a2f1261e5ad769170c6e9cf12",
16
+ "86cf732195aad676898520192ac8aa878267877dc09f85fcf425279d8badc9b8",
17
+ "910792f383c25cd46ddc7d851080a687fa532a93d605fb709b7baff9bb2789cf",
18
+ "95bdc516c4e1b72833af3bec13b18e3c2ac6ae83de284c3f711e97765ed1b461",
19
+ "a748564f0bba03effbf4aa381308f60eedcb618cbd2a37e1c67cdbee596e9ceb",
20
+ "a8a57b5892e7a5063078d561fce790b44d900d2bb3869e43ed4c4de82f1f62ed",
21
+ "cd03df57f7861d19b0a8e0a97e8aff1fa08e5c26d4f43152743968245632535b",
22
+ "d26fb42c6bee416eaf29bc2ed8579edc7a28ca3d0e18433dcb8abea11fac9920",
23
+ "d2d171e4ef1777facdb6fe665105372ea2e6452028d4c32f5e2ccc74dacc8850",
24
+ "d538d9eebaf181687ebf8537fc6ce7c8fae59fa424daeb45c3f900a8e210ecfc",
25
+ "d6c2cefddabd773d471138aad619ee9f90d0f39f18053efa4d43d8771668e410",
26
+ "df0be5a293b5259b02438e2555832a114548fed03ecdde5b80bbc621c800998d",
27
+ "df8e3014e583d60922cd1b7f7f16964c433b260a7e1a7e6c285cb08ce2d9e7e4",
28
+ "e70ca2a537cec56f8424ff4d551c1e97af133938fc7ba863a3944859dcfd194d",
29
+ "f0211acbaaa312bd5a4478bba81fa98293c855692ceca69f141600ff991c5904",
30
+ "f09f9b35ddfb17a7c4e4439384afae93ded6ee0285e778ad013d320cfc880cab",
31
+ "fa88198c5434613ff5ab2d44dce6e7eb8b825085b5b498402810eb144cf81f96"
32
+ ]
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { createHash } = require('crypto');
5
+ const legacyHashes = new Set(require('./legacy-asset-hashes.json'));
6
+ // Older combined installations predate the ownership manifest. Never leave a
7
+ // locally modified obsolete Replay bridge compiling beside the new bridge.
8
+ const legacyReplayNames = new Set(['FTCocosReplayImageJobs.java', 'FTCocosReplayImageJobs.h', 'FTCocosReplayImageJobs.m', 'FTReplayFileBridge.h', 'FTReplayFileWorker.h']);
9
+ const manifestName = '.cocos-sdk-managed.json';
10
+ const hash = bytes => createHash('sha256').update(bytes).digest('hex');
11
+
12
+ function files(root) {
13
+ const result = [];
14
+ if (!fs.existsSync(root)) return result;
15
+ function visit(directory) {
16
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
17
+ if (entry.name === manifestName || entry.name === '.DS_Store') continue;
18
+ const file = path.join(directory, entry.name);
19
+ if (entry.isSymbolicLink()) throw new Error(`[cocos-sdk] Refusing to copy symbolic link: ${file}`);
20
+ if (entry.isDirectory()) visit(file);
21
+ else result.push(path.relative(root, file).split(path.sep).join('/'));
22
+ }
23
+ }
24
+ visit(root);
25
+ return result;
26
+ }
27
+
28
+ /** Reconciles only files written by the SDK, preserving unrelated project assets. */
29
+ function syncManagedDirectory(destination, sources, overrides = {}) {
30
+ const desired = new Map();
31
+ for (const { root, prefix = '' } of sources) {
32
+ if (!fs.existsSync(root)) throw new Error(`[cocos-sdk] Missing package assets: ${root}`);
33
+ for (const file of files(root)) desired.set(prefix + file, fs.readFileSync(path.join(root, file)));
34
+ }
35
+ for (const [file, content] of Object.entries(overrides)) desired.set(file, Buffer.from(content));
36
+ const manifest = path.join(destination, manifestName);
37
+ const previous = fs.existsSync(manifest) ? JSON.parse(fs.readFileSync(manifest, 'utf8')) : {};
38
+ const existing = files(destination);
39
+ const remove = [];
40
+ // Check every conflict before writing or deleting any managed file.
41
+ for (const file of existing) {
42
+ const digest = hash(fs.readFileSync(path.join(destination, file)));
43
+ const old = previous[file];
44
+ const next = desired.get(file);
45
+ const legacy = legacyHashes.has(digest);
46
+ if (!next && !old && !legacy && legacyReplayNames.has(path.basename(file))) {
47
+ throw new Error(`[cocos-sdk] Preserve or reconcile modified legacy Replay asset before reinstalling: ${path.join(destination, file)}`);
48
+ }
49
+ if ((old || next) && digest !== old && !legacy && (!next || digest !== hash(next))) {
50
+ throw new Error(`[cocos-sdk] Preserve or reconcile modified SDK asset before reinstalling: ${path.join(destination, file)}`);
51
+ }
52
+ if (!next && (old || legacy) && !file.endsWith('.meta')) remove.push(file);
53
+ }
54
+ fs.mkdirSync(destination, { recursive: true });
55
+ for (const file of remove) fs.unlinkSync(path.join(destination, file));
56
+ const nextManifest = {};
57
+ for (const [file, content] of desired) {
58
+ const target = path.resolve(destination, file);
59
+ if (!target.startsWith(path.resolve(destination) + path.sep)) throw new Error('Invalid SDK asset path');
60
+ fs.mkdirSync(path.dirname(target), { recursive: true });
61
+ if (!fs.existsSync(target) || hash(fs.readFileSync(target)) !== hash(content)) fs.writeFileSync(target, content);
62
+ nextManifest[file] = hash(content);
63
+ }
64
+ fs.writeFileSync(manifest, `${JSON.stringify(nextManifest, null, 2)}\n`);
65
+ }
66
+
67
+ module.exports = { syncManagedDirectory };