@leonxin/meetgames 0.1.12 → 0.1.14

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 (49) hide show
  1. package/dist/android/manifest.d.ts.map +1 -1
  2. package/dist/android/manifest.js +9 -0
  3. package/dist/android/manifest.js.map +1 -1
  4. package/dist/android/meetSdkRemoteGradle.d.ts.map +1 -1
  5. package/dist/android/meetSdkRemoteGradle.js +126 -6
  6. package/dist/android/meetSdkRemoteGradle.js.map +1 -1
  7. package/dist/contracts/types.d.ts +3 -0
  8. package/dist/contracts/types.d.ts.map +1 -1
  9. package/dist/core/pipeline.d.ts.map +1 -1
  10. package/dist/core/pipeline.js +3 -0
  11. package/dist/core/pipeline.js.map +1 -1
  12. package/dist/core/reporter.d.ts.map +1 -1
  13. package/dist/core/reporter.js +4 -0
  14. package/dist/core/reporter.js.map +1 -1
  15. package/dist/ios/channelConfig.d.ts.map +1 -1
  16. package/dist/ios/channelConfig.js +18 -2
  17. package/dist/ios/channelConfig.js.map +1 -1
  18. package/dist/ios/codeUtils.d.ts +1 -0
  19. package/dist/ios/codeUtils.d.ts.map +1 -1
  20. package/dist/ios/codeUtils.js +3 -0
  21. package/dist/ios/codeUtils.js.map +1 -1
  22. package/dist/ios/integrate.d.ts.map +1 -1
  23. package/dist/ios/integrate.js +242 -97
  24. package/dist/ios/integrate.js.map +1 -1
  25. package/dist/ios/pbxprojEditor.d.ts.map +1 -1
  26. package/dist/ios/pbxprojEditor.js +52 -1
  27. package/dist/ios/pbxprojEditor.js.map +1 -1
  28. package/dist/ops/handlers.d.ts.map +1 -1
  29. package/dist/ops/handlers.js +35 -3
  30. package/dist/ops/handlers.js.map +1 -1
  31. package/docs/API.md +1 -1
  32. package/docs/INTEGRATION.md +48 -1
  33. package/package.json +1 -1
  34. package/src/android/manifest.ts +11 -0
  35. package/src/android/meetSdkRemoteGradle.ts +125 -7
  36. package/src/contracts/types.ts +3 -0
  37. package/src/core/pipeline.ts +3 -0
  38. package/src/core/reporter.ts +3 -0
  39. package/src/ios/channelConfig.ts +18 -2
  40. package/src/ios/codeUtils.ts +4 -0
  41. package/src/ios/integrate.ts +253 -96
  42. package/src/ios/pbxprojEditor.ts +51 -1
  43. package/src/ops/handlers.ts +38 -3
  44. package/tests/doctor.test.ts +43 -3
  45. package/tests/meetSdkRemoteConfig.test.ts +1 -1
  46. package/tests/meetSdkRemoteGradle.test.ts +2 -2
  47. package/tests/pipeline.android.test.ts +64 -10
  48. package/tests/pipeline.ios.test.ts +219 -32
  49. package/tests/platformSelection.test.ts +4 -4
@@ -31,6 +31,7 @@ import {
31
31
  addSystemLib,
32
32
  addThirdPartyFramework,
33
33
  findInfoPlistPathsFromPbx,
34
+ getTargetBuildSettings,
34
35
  loadPbxFromStore,
35
36
  savePbxToStore,
36
37
  setBuildSetting,
@@ -50,12 +51,35 @@ export interface IosIntegrateOptions {
50
51
 
51
52
  const IOS_FIREBASE_CONFIG_FILE = "GoogleService-Info.plist";
52
53
 
53
- function ok(changed: string[], warnings: string[] = []): StepResult {
54
- return { ok: true, changedFiles: changed, warnings, errors: [] };
54
+ function logValue(value: unknown): string {
55
+ if (value === undefined) return "<missing>";
56
+ if (typeof value === "string") return value;
57
+ return JSON.stringify(value);
55
58
  }
56
59
 
57
- function fail(errors: string[], warnings: string[] = []): StepResult {
58
- return { ok: false, changedFiles: [], warnings, errors };
60
+ function pushValueChangeLog(logs: string[], label: string, before: unknown, after: unknown): void {
61
+ if (valuesEqual(before, after)) {
62
+ logs.push(`SUCCESS: ${label} 未变化 value:${logValue(after)}`);
63
+ return;
64
+ }
65
+ logs.push(`SUCCESS: ${label} ${logValue(before)} -> ${logValue(after)}`);
66
+ }
67
+
68
+ function valuesEqual(before: unknown, after: unknown): boolean {
69
+ return JSON.stringify(before) === JSON.stringify(after);
70
+ }
71
+
72
+ function cloneValue(value: unknown): unknown {
73
+ if (value === undefined) return undefined;
74
+ return JSON.parse(JSON.stringify(value)) as unknown;
75
+ }
76
+
77
+ function ok(changed: string[], warnings: string[] = [], logs: string[] = []): StepResult {
78
+ return { ok: true, changedFiles: changed, logs, warnings, errors: [] };
79
+ }
80
+
81
+ function fail(errors: string[], warnings: string[] = [], logs: string[] = []): StepResult {
82
+ return { ok: false, changedFiles: [], logs, warnings, errors };
59
83
  }
60
84
 
61
85
  function applyPlugin(
@@ -66,15 +90,18 @@ function applyPlugin(
66
90
  perform: readonly PerformSetting[],
67
91
  binaryCopies: BinaryCopy[],
68
92
  dryRun: boolean,
93
+ logs: string[],
69
94
  remoteSources: Map<string, string> = new Map()
70
95
  ): void {
71
96
  const { config, sourceDir } = loaded;
72
- const srcRoot = pbx.srcRoot;
97
+
98
+ logs.push(`- 插件:${config.name}, 类型:${config.type},版本:${config.pluginVersion}`);
73
99
 
74
100
  if (perform.includes("frameworks") && config.frameworks?.length) {
75
101
  for (const fw of config.frameworks) {
76
102
  if (fw.system) {
77
103
  addSystemFramework(pbx, fw.name);
104
+ logs.push(`SUCCESS: 导入系统依赖framework ${fw.name} 完成`);
78
105
  continue;
79
106
  }
80
107
  const src = path.join(sourceDir, fw.name);
@@ -86,6 +113,7 @@ function applyPlugin(
86
113
  fm.copyFramework(src);
87
114
  }
88
115
  addThirdPartyFramework(pbx, relTo, Boolean(fw.embed));
116
+ logs.push(`SUCCESS: 导入三方依赖framework ${fw.name} 完成`);
89
117
  if (fw.copyFile) addCopyFile(pbx, relTo);
90
118
  }
91
119
  }
@@ -94,6 +122,7 @@ function applyPlugin(
94
122
  for (const lib of config.libs) {
95
123
  if (lib.system) {
96
124
  addSystemLib(pbx, lib.name);
125
+ logs.push(`SUCCESS: 导入系统依赖lib ${lib.name} 完成`);
97
126
  continue;
98
127
  }
99
128
  const src = path.join(sourceDir, lib.name);
@@ -102,6 +131,7 @@ function applyPlugin(
102
131
  if (dryRun) binaryCopies.push({ fromAbs: src, relTo });
103
132
  else fm.copyFile(src);
104
133
  addSourceOrResourceFile(pbx, relTo);
134
+ logs.push(`SUCCESS: 导入三方依赖lib ${lib.name} 完成`);
105
135
  }
106
136
  }
107
137
 
@@ -110,6 +140,7 @@ function applyPlugin(
110
140
  const remoteRel = remoteSources.get(`${config.name}:${source}`);
111
141
  if (remoteRel) {
112
142
  addSourceOrResourceFile(pbx, remoteRel);
143
+ logs.push(`SUCCESS: 引入资源 ${source} 完成`);
113
144
  continue;
114
145
  }
115
146
  const src = path.join(sourceDir, source);
@@ -126,15 +157,17 @@ function applyPlugin(
126
157
  for (const name of fs.readdirSync(dir)) {
127
158
  const full = path.join(dir, name);
128
159
  if (fs.statSync(full).isDirectory()) {
129
- if (name.endsWith(".bundle")) {
130
- addSourceOrResourceFile(pbx, fm.relFromSrc(full));
131
- } else {
132
- walk(full);
133
- }
160
+ if (name.endsWith(".bundle")) {
161
+ addSourceOrResourceFile(pbx, fm.relFromSrc(full));
162
+ logs.push(`SUCCESS: 引入资源 ${name} 完成`);
163
+ } else {
164
+ walk(full);
165
+ }
134
166
  continue;
135
167
  }
136
168
  if (/\.(h|m|mm|plist|a)$/.test(name)) {
137
169
  addSourceOrResourceFile(pbx, fm.relFromSrc(full));
170
+ logs.push(`SUCCESS: 引入资源 ${name} 完成`);
138
171
  }
139
172
  }
140
173
  };
@@ -146,22 +179,30 @@ function applyPlugin(
146
179
  else if (fs.statSync(src).isDirectory()) fm.copyDir(src);
147
180
  else fm.copyFile(src);
148
181
  addSourceOrResourceFile(pbx, relTo);
182
+ logs.push(`SUCCESS: 引入资源 ${source} 完成`);
149
183
  }
150
184
  }
151
185
  }
152
186
 
153
187
  if (perform.includes("buildSettings") && config.buildSettings) {
154
188
  for (const [k, v] of Object.entries(config.buildSettings)) {
155
- setBuildSetting(pbx, k, v);
189
+ const beforeValues = [...new Set(getTargetBuildSettings(pbx).map((settings) => settings[k]))];
190
+ if (beforeValues.length === 0 || beforeValues.some((before) => !valuesEqual(before, v))) {
191
+ setBuildSetting(pbx, k, v);
192
+ }
193
+ for (const before of beforeValues.length ? beforeValues : [undefined]) {
194
+ pushValueChangeLog(logs, `BuildSetting设置${k}`, before, v);
195
+ }
196
+ logs.push(`SUCCESS: BuildSetting设置${k}为${v} 完成`);
156
197
  }
157
198
  }
158
199
 
159
200
  if (perform.includes("OtherLinkerFlags") && config.OtherLinkerFlags?.length) {
160
201
  for (const flag of config.OtherLinkerFlags) {
161
202
  addOtherLinkerFlag(pbx, flag);
203
+ logs.push(`SUCCESS: 添加other link flag:${flag} 完成`);
162
204
  }
163
205
  }
164
-
165
206
  }
166
207
 
167
208
  function validateLoadedPluginResourcesForIos(loaded: LoadedPluginConfig): string[] {
@@ -224,6 +265,42 @@ function iosFirebaseConfigRelPath(plistDocs: PlistDocument[]): string {
224
265
  return dir === "." ? IOS_FIREBASE_CONFIG_FILE : path.posix.join(dir, IOS_FIREBASE_CONFIG_FILE);
225
266
  }
226
267
 
268
+ function hasSwiftSource(srcRoot: string): boolean {
269
+ const walk = (dir: string): boolean => {
270
+ let entries: fs.Dirent[];
271
+ try {
272
+ entries = fs.readdirSync(dir, { withFileTypes: true });
273
+ } catch {
274
+ return false;
275
+ }
276
+ for (const entry of entries) {
277
+ if (entry.name.startsWith(".")) continue;
278
+ const full = path.join(dir, entry.name);
279
+ if (entry.isDirectory()) {
280
+ if (!["Pods", "build", "DerivedData", "topSDK"].includes(entry.name) && walk(full)) return true;
281
+ continue;
282
+ }
283
+ if (entry.name.endsWith(".swift")) return true;
284
+ }
285
+ return false;
286
+ };
287
+ return walk(srcRoot);
288
+ }
289
+
290
+ function ensureTopSdkInstallSwift(
291
+ store: TextFileStore,
292
+ ctx: WorkspaceContext,
293
+ pbx: PbxContext
294
+ ): string | null {
295
+ if (hasSwiftSource(pbx.srcRoot)) return null;
296
+ const relFromSrc = path.join(pbx.targetName, "TopSDKInstall.swift").split(path.sep).join("/");
297
+ const abs = path.join(pbx.srcRoot, relFromSrc);
298
+ const relFromProject = path.relative(ctx.projectRoot, abs).split(path.sep).join("/");
299
+ store.write(relFromProject, "import Foundation\n\n");
300
+ addSourceOrResourceFile(pbx, relFromSrc);
301
+ return relFromProject;
302
+ }
303
+
227
304
  function appDelegateCodeShouldMoveToSceneDelegate(code: CodeConfig): boolean {
228
305
  if (code.type === "header") return false;
229
306
  const method = code.method ?? "";
@@ -274,7 +351,9 @@ function mergePlistParams(
274
351
  docs: PlistDocument[],
275
352
  loaded: LoadedPluginConfig,
276
353
  channelConfig: Record<string, unknown>,
277
- perform: readonly PerformSetting[]
354
+ perform: readonly PerformSetting[],
355
+ logs: string[],
356
+ dirtyDocs: Set<PlistDocument>
278
357
  ): void {
279
358
  if (!perform.includes("infoParams") && !perform.includes("urlScheme") && !perform.includes("queriesSchemes")) {
280
359
  return;
@@ -282,18 +361,33 @@ function mergePlistParams(
282
361
  for (const doc of docs) {
283
362
  if (perform.includes("infoParams") && loaded.config.infoParams) {
284
363
  for (const [key, raw] of Object.entries(loaded.config.infoParams)) {
285
- addPlistParam(doc.data, key, applyChannelTemplateValue(raw, channelConfig));
364
+ const value = applyChannelTemplateValue(raw, channelConfig);
365
+ const before = doc.data[key];
366
+ if (!valuesEqual(before, value)) {
367
+ addPlistParam(doc.data, key, value);
368
+ dirtyDocs.add(doc);
369
+ }
370
+ pushValueChangeLog(logs, `info.plist配置key:${key}`, before, value);
371
+ logs.push(`SUCCESS: info.plist添加配置key:${key} value:${String(value)} 完成`);
286
372
  }
287
373
  }
288
374
  if (perform.includes("queriesSchemes") && loaded.config.queriesSchemes?.length) {
289
375
  for (const scheme of loaded.config.queriesSchemes) {
376
+ const before = cloneValue(doc.data.LSApplicationQueriesSchemes ?? []);
290
377
  addQueriesScheme(doc.data, scheme);
378
+ if (!valuesEqual(before, doc.data.LSApplicationQueriesSchemes)) dirtyDocs.add(doc);
379
+ pushValueChangeLog(logs, `info.plist queriesSchemes ${scheme}`, before, doc.data.LSApplicationQueriesSchemes);
380
+ logs.push(`SUCCESS: 添加queriesSchemes:${scheme} 完成`);
291
381
  }
292
382
  }
293
383
  if (perform.includes("urlScheme") && loaded.config.urlScheme) {
294
384
  let scheme = applyChannelTemplate(loaded.config.urlScheme, channelConfig);
295
385
  if (scheme.endsWith("://")) scheme = scheme.replace("://", "");
386
+ const before = cloneValue(doc.data.CFBundleURLTypes ?? []);
296
387
  addUrlScheme(doc.data, scheme);
388
+ if (!valuesEqual(before, doc.data.CFBundleURLTypes)) dirtyDocs.add(doc);
389
+ pushValueChangeLog(logs, `info.plist URL Scheme ${scheme}`, before, doc.data.CFBundleURLTypes);
390
+ logs.push(`SUCCESS: 设置urlScheme:${scheme}完成`);
297
391
  }
298
392
  }
299
393
  }
@@ -323,6 +417,89 @@ function buildTopSdkPluginInfo(
323
417
  return { pluginsInfo, dataPluginsInfo };
324
418
  }
325
419
 
420
+ function injectDelegateCodes(
421
+ loadedConfigs: LoadedPluginConfig[],
422
+ ctx: WorkspaceContext,
423
+ store: TextFileStore,
424
+ pbx: PbxContext,
425
+ warnings: string[],
426
+ changed: Set<string>,
427
+ logs: string[]
428
+ ): void {
429
+ const hasAppDelegateCodes = loadedConfigs.some((loaded) => (loaded.config.appDelegateCodes ?? []).length > 0);
430
+ const hasSceneDelegateCodes = loadedConfigs.some((loaded) => (loaded.config.sceneDelegateCodes ?? []).length > 0);
431
+ if (!hasAppDelegateCodes && !hasSceneDelegateCodes) return;
432
+
433
+ const sceneDelegateRelPaths: string[] = [];
434
+ for (const abs of findSceneDelegateFiles(pbx.srcRoot)) {
435
+ sceneDelegateRelPaths.push(path.relative(ctx.projectRoot, abs).split(path.sep).join("/"));
436
+ }
437
+ const hasUniqueSceneDelegate = sceneDelegateRelPaths.length === 1;
438
+
439
+ const delegateRelPaths: string[] = [];
440
+ for (const abs of findDelegateFiles(pbx.srcRoot)) {
441
+ delegateRelPaths.push(path.relative(ctx.projectRoot, abs).split(path.sep).join("/"));
442
+ }
443
+ if (hasAppDelegateCodes && !delegateRelPaths.length) {
444
+ warnings.push("no UIApplicationDelegate .m/.mm found; skipped AppDelegate injection (SwiftUI @main is not supported yet)");
445
+ }
446
+ if (hasAppDelegateCodes && delegateRelPaths.length > 1) {
447
+ warnings.push(`multiple UIApplicationDelegate files found; skipped AppDelegate injection: ${delegateRelPaths.join(", ")}`);
448
+ }
449
+ for (const rel of hasAppDelegateCodes && delegateRelPaths.length === 1 ? delegateRelPaths : []) {
450
+ const cu = CodeUtils.fromFile(path.join(ctx.projectRoot, rel));
451
+ logs.push("检测到工程UIApplicationDelegate实现类,将自动接入对应接口");
452
+ let okInject = true;
453
+ for (const loaded of loadedConfigs) {
454
+ for (const code of appDelegateCodesForLifecycle(loaded, hasUniqueSceneDelegate)) {
455
+ const existed = cu.hasCode(code.content);
456
+ if (code.type === "header") {
457
+ if (!cu.addHeader(code.content)) okInject = false;
458
+ } else if ((code.type === "method" || code.type === "code") && code.method) {
459
+ if (!cu.addCodeToMethod(code.method, code.content, Boolean(code.addToReturn))) okInject = false;
460
+ }
461
+ if (okInject) {
462
+ logs.push(
463
+ `SUCCESS: 代码文件${path.join(ctx.projectRoot, rel)},${existed ? "已存在代码" : "添加代码"}${code.content}${existed ? "" : "完成"}`
464
+ );
465
+ }
466
+ }
467
+ }
468
+ if (!okInject) warnings.push(`AppDelegate injection incomplete for ${rel}`);
469
+ const before = store.read(rel);
470
+ cu.applyToStore((c) => store.write(rel, c));
471
+ if (store.read(rel) !== before) changed.add(rel);
472
+ }
473
+
474
+ if (hasSceneDelegateCodes && sceneDelegateRelPaths.length > 1) {
475
+ warnings.push(`multiple UIWindowSceneDelegate files found; skipped SceneDelegate injection: ${sceneDelegateRelPaths.join(", ")}`);
476
+ }
477
+ for (const rel of hasSceneDelegateCodes && sceneDelegateRelPaths.length === 1 ? sceneDelegateRelPaths : []) {
478
+ const cu = CodeUtils.fromFile(path.join(ctx.projectRoot, rel));
479
+ logs.push("检测到工程存在UIWindowSceneDelegate实现类,将自动接入对应接口");
480
+ let okInject = true;
481
+ for (const loaded of loadedConfigs) {
482
+ for (const code of loaded.config.sceneDelegateCodes ?? []) {
483
+ const existed = cu.hasCode(code.content);
484
+ if (code.type === "header") {
485
+ if (!cu.addHeader(code.content)) okInject = false;
486
+ } else if ((code.type === "method" || code.type === "code") && code.method) {
487
+ if (!cu.addCodeToMethod(code.method, code.content, Boolean(code.addToReturn))) okInject = false;
488
+ }
489
+ if (okInject) {
490
+ logs.push(
491
+ `SUCCESS: 代码文件${path.join(ctx.projectRoot, rel)},${existed ? "已存在代码" : "添加代码"}${code.content}${existed ? "" : "完成"}`
492
+ );
493
+ }
494
+ }
495
+ }
496
+ if (!okInject) warnings.push(`SceneDelegate injection incomplete for ${rel}`);
497
+ const before = store.read(rel);
498
+ cu.applyToStore((c) => store.write(rel, c));
499
+ if (store.read(rel) !== before) changed.add(rel);
500
+ }
501
+ }
502
+
326
503
  export async function runIosIntegrateTopSdk(
327
504
  ctx: WorkspaceContext,
328
505
  store: TextFileStore,
@@ -331,20 +508,21 @@ export async function runIosIntegrateTopSdk(
331
508
  ): Promise<StepResult> {
332
509
  if (!ctx.ios?.ok) return fail(["iOS project not detected"]);
333
510
  const warnings: string[] = [];
511
+ const logs: string[] = [];
334
512
  const changed = new Set<string>();
335
513
 
336
514
  const configPath = ctx.remoteConfigPath ?? path.join(ctx.projectRoot, MEETSDK_REMOTE_CONFIG_FILENAME);
337
515
  if (!fs.existsSync(configPath)) {
338
- return fail([`${MEETSDK_REMOTE_CONFIG_FILENAME} not found at ${configPath}; run fetch-config or setup first`]);
516
+ return fail([`${MEETSDK_REMOTE_CONFIG_FILENAME} not found at ${configPath}; run fetch-config or setup first`], warnings, logs);
339
517
  }
340
518
  let remote: ReturnType<typeof tryParseAsMeetSdkRemoteConfig>;
341
519
  try {
342
520
  remote = tryParseAsMeetSdkRemoteConfig(JSON.parse(fs.readFileSync(configPath, "utf8")) as unknown);
343
521
  } catch (e) {
344
- return fail([e instanceof Error ? e.message : String(e)]);
522
+ return fail([e instanceof Error ? e.message : String(e)], warnings, logs);
345
523
  }
346
524
  if (!remote) {
347
- return fail([`invalid ${MEETSDK_REMOTE_CONFIG_FILENAME}`]);
525
+ return fail([`invalid ${MEETSDK_REMOTE_CONFIG_FILENAME}`], warnings, logs);
348
526
  }
349
527
  const validation = validateMeetSdkRemoteConfig(remote);
350
528
  if (!validation.ok) {
@@ -355,13 +533,15 @@ export async function runIosIntegrateTopSdk(
355
533
  try {
356
534
  sdkRoot = ctx.iosSdkRoot ?? resolveIosSdkRoot(ctx.packageRoot);
357
535
  } catch (e) {
358
- return fail([e instanceof Error ? e.message : String(e)]);
536
+ return fail([e instanceof Error ? e.message : String(e)], warnings, logs);
359
537
  }
360
538
 
361
539
  const targetName = options.targetName ?? ctx.ios.targetName ?? path.basename(ctx.ios.xcodeprojPath!, ".xcodeproj");
362
540
  const perform = options.performSettings ?? DEFAULT_PERFORM_SETTINGS;
363
541
  const executeAppDelegate = options.executeAppDelegate !== false;
364
542
  const dryRun = Boolean(options.dryRun);
543
+ logs.push("6、执行 SDK 接入");
544
+ logs.push(`*** 项目根目录:${ctx.projectRoot} ***`);
365
545
 
366
546
  const channelConfig = buildChannelConfigMap(remote);
367
547
  const coreConfigs = listSdkCoreConfigs(sdkRoot);
@@ -384,50 +564,82 @@ export async function runIosIntegrateTopSdk(
384
564
  try {
385
565
  plistDocs = await ensureInfoPlists(store, ctx, pbx, targetName);
386
566
  } catch (e) {
387
- return fail([e instanceof Error ? e.message : String(e)], warnings);
567
+ return fail([e instanceof Error ? e.message : String(e)], warnings, logs);
388
568
  }
569
+ const dirtyPlistDocs = new Set<PlistDocument>();
389
570
 
390
571
  const firebaseDownload = await downloadIosFirebaseConfig(remote, pbx, dryRun, iosFirebaseConfigRelPath(plistDocs));
391
572
  warnings.push(...firebaseDownload.warnings);
392
- if (firebaseDownload.errors.length) return fail(firebaseDownload.errors, warnings);
573
+ if (firebaseDownload.errors.length) return fail(firebaseDownload.errors, warnings, logs);
393
574
 
394
575
  const resourceErrors = [...coreConfigs, ...pluginConfigs].flatMap(validateLoadedPluginResourcesForIos);
395
- if (resourceErrors.length) return fail(resourceErrors, warnings);
576
+ if (resourceErrors.length) return fail(resourceErrors, warnings, logs);
396
577
 
397
578
  const missingRequiredConfigs = validateRequiredChannelConfigs(pluginConfigs, channelConfig);
398
579
  if (missingRequiredConfigs.length) {
399
- return fail([`iOS remote config missing required channel params: ${missingRequiredConfigs.join(", ")}`], warnings);
580
+ return fail([`iOS remote config missing required channel params: ${missingRequiredConfigs.join(", ")}`], warnings, logs);
400
581
  }
401
582
 
402
- for (const loaded of coreConfigs) {
403
- applyPlugin(loaded, pbx, fm, channelConfig, perform, binaryCopies, dryRun, firebaseDownload.remoteSources);
404
- }
405
583
  for (const loaded of pluginConfigs) {
406
- applyPlugin(loaded, pbx, fm, channelConfig, perform, binaryCopies, dryRun, firebaseDownload.remoteSources);
584
+ applyPlugin(loaded, pbx, fm, channelConfig, perform, binaryCopies, dryRun, logs, firebaseDownload.remoteSources);
585
+ if (loaded.config.name === "FacebookSignin" && perform.includes("sources")) {
586
+ const swiftRel = ensureTopSdkInstallSwift(store, ctx, pbx);
587
+ if (swiftRel) {
588
+ changed.add(swiftRel);
589
+ logs.push("SUCCESS: 未检测到Swift代码文件,自动添加TopSDKInstall.swift完成");
590
+ }
591
+ }
592
+ mergePlistParams(plistDocs, loaded, channelConfig, perform, logs, dirtyPlistDocs);
593
+ if (perform.includes("infoParams")) {
594
+ const doc = plistDocs[0];
595
+ if (doc) {
596
+ const before = cloneValue(doc.data.NSAppTransportSecurity);
597
+ setAppTransportSecurity(doc.data, true);
598
+ if (!valuesEqual(before, doc.data.NSAppTransportSecurity)) dirtyPlistDocs.add(doc);
599
+ }
600
+ }
601
+ if (perform.includes("infoParams") && loaded.config.name === "AppleSignin") {
602
+ for (const rel of ensureAppleSignInEntitlement(store, ctx.projectRoot, pbx)) {
603
+ changed.add(rel);
604
+ }
605
+ logs.push("SUCCESS: 启用SigninWithApple完成");
606
+ }
607
+ logs.push(`SUCCESS: 插件${loaded.config.name}接入完成`);
407
608
  }
408
-
409
- for (const loaded of [...coreConfigs, ...pluginConfigs]) {
410
- mergePlistParams(plistDocs, loaded, channelConfig, perform);
609
+ for (const loaded of coreConfigs) {
610
+ applyPlugin(loaded, pbx, fm, channelConfig, perform, binaryCopies, dryRun, logs, firebaseDownload.remoteSources);
611
+ mergePlistParams(plistDocs, loaded, channelConfig, perform, logs, dirtyPlistDocs);
411
612
  if (perform.includes("infoParams")) {
412
- setAppTransportSecurity(plistDocs[0]?.data ?? {}, true);
613
+ const doc = plistDocs[0];
614
+ if (doc) {
615
+ const before = cloneValue(doc.data.NSAppTransportSecurity);
616
+ setAppTransportSecurity(doc.data, true);
617
+ if (!valuesEqual(before, doc.data.NSAppTransportSecurity)) dirtyPlistDocs.add(doc);
618
+ }
619
+ }
620
+ if (executeAppDelegate) {
621
+ injectDelegateCodes([loaded], ctx, store, pbx, warnings, changed, logs);
413
622
  }
623
+ logs.push(`SUCCESS: 插件${loaded.config.name}接入完成`);
414
624
  }
415
625
  for (const doc of plistDocs) {
416
626
  if (perform.includes("infoParams")) {
417
627
  const { pluginsInfo, dataPluginsInfo } = buildTopSdkPluginInfo(pluginConfigs, channelConfig);
418
- addPlistParam(doc.data, "TOPSDK", {
628
+ const before = doc.data.TOPSDK;
629
+ const nextTopSdk = {
419
630
  APP_ID: remote.topsdk.appId,
420
631
  Plugins: pluginsInfo,
421
632
  dataPlugins: dataPluginsInfo,
422
- });
633
+ };
634
+ if (!valuesEqual(before, nextTopSdk)) {
635
+ addPlistParam(doc.data, "TOPSDK", nextTopSdk);
636
+ dirtyPlistDocs.add(doc);
637
+ }
638
+ pushValueChangeLog(logs, "info.plist配置key:TOPSDK", before, nextTopSdk);
423
639
  }
424
640
  const rel = path.relative(ctx.projectRoot, path.join(pbx.srcRoot, doc.relPath)).split(path.sep).join("/");
425
- store.write(rel, buildPlistXml(doc.data));
426
- changed.add(rel);
427
- }
428
-
429
- if (perform.includes("infoParams") && pluginConfigs.some((p) => p.config.name === "AppleSignin")) {
430
- for (const rel of ensureAppleSignInEntitlement(store, ctx.projectRoot, pbx)) {
641
+ if (dirtyPlistDocs.has(doc)) {
642
+ store.write(rel, buildPlistXml(doc.data));
431
643
  changed.add(rel);
432
644
  }
433
645
  }
@@ -435,64 +647,9 @@ export async function runIosIntegrateTopSdk(
435
647
  savePbxToStore(store, pbx);
436
648
  changed.add(pbx.rel);
437
649
 
438
- if (executeAppDelegate) {
439
- const sceneDelegateRelPaths: string[] = [];
440
- for (const abs of findSceneDelegateFiles(pbx.srcRoot)) {
441
- sceneDelegateRelPaths.push(path.relative(ctx.projectRoot, abs).split(path.sep).join("/"));
442
- }
443
- const hasUniqueSceneDelegate = sceneDelegateRelPaths.length === 1;
444
-
445
- const delegateRelPaths: string[] = [];
446
- for (const abs of findDelegateFiles(pbx.srcRoot)) {
447
- delegateRelPaths.push(path.relative(ctx.projectRoot, abs).split(path.sep).join("/"));
448
- }
449
- if (!delegateRelPaths.length) {
450
- warnings.push("no UIApplicationDelegate .m/.mm found; skipped AppDelegate injection (SwiftUI @main is not supported yet)");
451
- }
452
- if (delegateRelPaths.length > 1) {
453
- warnings.push(`multiple UIApplicationDelegate files found; skipped AppDelegate injection: ${delegateRelPaths.join(", ")}`);
454
- }
455
- for (const rel of delegateRelPaths.length === 1 ? delegateRelPaths : []) {
456
- const cu = CodeUtils.fromFile(path.join(ctx.projectRoot, rel));
457
- let okInject = true;
458
- for (const loaded of [...coreConfigs, ...pluginConfigs]) {
459
- for (const code of appDelegateCodesForLifecycle(loaded, hasUniqueSceneDelegate)) {
460
- if (code.type === "header") {
461
- if (!cu.addHeader(code.content)) okInject = false;
462
- } else if ((code.type === "method" || code.type === "code") && code.method) {
463
- if (!cu.addCodeToMethod(code.method, code.content, Boolean(code.addToReturn))) okInject = false;
464
- }
465
- }
466
- }
467
- if (!okInject) warnings.push(`AppDelegate injection incomplete for ${rel}`);
468
- const before = store.read(rel);
469
- cu.applyToStore((c) => store.write(rel, c));
470
- if (store.read(rel) !== before) changed.add(rel);
471
- }
472
-
473
- if (sceneDelegateRelPaths.length > 1) {
474
- warnings.push(`multiple UIWindowSceneDelegate files found; skipped SceneDelegate injection: ${sceneDelegateRelPaths.join(", ")}`);
475
- }
476
- for (const rel of sceneDelegateRelPaths.length === 1 ? sceneDelegateRelPaths : []) {
477
- const cu = CodeUtils.fromFile(path.join(ctx.projectRoot, rel));
478
- let okInject = true;
479
- for (const loaded of [...coreConfigs, ...pluginConfigs]) {
480
- for (const code of loaded.config.sceneDelegateCodes ?? []) {
481
- if (code.type === "header") {
482
- if (!cu.addHeader(code.content)) okInject = false;
483
- } else if ((code.type === "method" || code.type === "code") && code.method) {
484
- if (!cu.addCodeToMethod(code.method, code.content, Boolean(code.addToReturn))) okInject = false;
485
- }
486
- }
487
- }
488
- if (!okInject) warnings.push(`SceneDelegate injection incomplete for ${rel}`);
489
- const before = store.read(rel);
490
- cu.applyToStore((c) => store.write(rel, c));
491
- if (store.read(rel) !== before) changed.add(rel);
492
- }
493
- }
494
-
495
- return ok([...changed], warnings);
650
+ logs.push("SUCCESS: info.plist配置写入完成");
651
+ logs.push("!! 接入流程已全部结束 !!");
652
+ return ok([...changed], warnings, logs);
496
653
  }
497
654
 
498
655
  async function ensureInfoPlists(
@@ -290,7 +290,12 @@ export function addSourceOrResourceFile(ctx: PbxContext, relPathFromSrcRoot: str
290
290
  if (file.endsWith(".h")) {
291
291
  ctx.proj.addHeaderFile(file, { target });
292
292
  } else if (file.endsWith(".m") || file.endsWith(".mm") || file.endsWith(".swift")) {
293
- ctx.proj.addSourceFile(file, { target });
293
+ try {
294
+ ctx.proj.addSourceFile(file, { target });
295
+ } catch (e) {
296
+ if (!String(e instanceof Error ? e.message : e).includes("path")) throw e;
297
+ addSourceFileManually(ctx, file);
298
+ }
294
299
  } else {
295
300
  ensureResourcesBuildPhase(ctx.proj, ctx.targetName);
296
301
  const opt: Record<string, unknown> = { target };
@@ -306,6 +311,51 @@ export function addSourceOrResourceFile(ctx: PbxContext, relPathFromSrcRoot: str
306
311
  }
307
312
  }
308
313
 
314
+ function ensureSourcesBuildPhase(proj: XcodeProject, targetName: string): void {
315
+ const target = targetKey(proj, targetName);
316
+ try {
317
+ proj.pbxSourcesBuildPhaseObj(target);
318
+ return;
319
+ } catch {
320
+ proj.addBuildPhase([], "PBXSourcesBuildPhase", "Sources", target);
321
+ }
322
+ }
323
+
324
+ function sourceFileType(file: string): string {
325
+ if (file.endsWith(".swift")) return "sourcecode.swift";
326
+ if (file.endsWith(".mm")) return "sourcecode.cpp.objcpp";
327
+ return "sourcecode.c.objc";
328
+ }
329
+
330
+ function addSourceFileManually(ctx: PbxContext, file: string): void {
331
+ const basename = path.basename(file);
332
+ ensureSourcesBuildPhase(ctx.proj, ctx.targetName);
333
+ const sources = ctx.proj.pbxSourcesBuildPhaseObj(targetKey(ctx.proj, ctx.targetName));
334
+ const files = (sources.files ??= []) as Array<{ value?: string; comment?: string }>;
335
+ if (JSON.stringify(files).includes(basename)) return;
336
+
337
+ const fileRefUuid = ctx.proj.generateUuid();
338
+ const buildUuid = ctx.proj.generateUuid();
339
+ const fileRefSection = objectSection(ctx.proj, "PBXFileReference");
340
+ const buildFileSection = objectSection(ctx.proj, "PBXBuildFile");
341
+
342
+ fileRefSection[fileRefUuid] = {
343
+ isa: "PBXFileReference",
344
+ name: `"${basename}"`,
345
+ path: `"${file}"`,
346
+ sourceTree: '"<group>"',
347
+ lastKnownFileType: sourceFileType(file),
348
+ };
349
+ fileRefSection[`${fileRefUuid}_comment`] = basename;
350
+ buildFileSection[buildUuid] = {
351
+ isa: "PBXBuildFile",
352
+ fileRef: fileRefUuid,
353
+ fileRef_comment: basename,
354
+ };
355
+ buildFileSection[`${buildUuid}_comment`] = `${basename} in Sources`;
356
+ files.push({ value: buildUuid, comment: `${basename} in Sources` });
357
+ }
358
+
309
359
  function resourceFileType(file: string, lastKnownFileType?: string): string {
310
360
  if (lastKnownFileType) return lastKnownFileType;
311
361
  if (file.endsWith(".bundle")) return "wrapper.plug-in";
@@ -25,14 +25,49 @@ import { loadMeetSdkDefaultConfigWithLatestAndroidVersion } from "../config/meet
25
25
  import { insertPermissions } from "../android/manifest.js";
26
26
  import type { TextFileStore } from "./fileStore.js";
27
27
 
28
- function ok(changed: string[], warnings: string[] = []): StepResult {
29
- return { ok: true, changedFiles: changed, warnings, errors: [] };
28
+ function ok(changed: string[], warnings: string[] = [], logs: string[] = []): StepResult {
29
+ return { ok: true, changedFiles: changed, logs, warnings, errors: [] };
30
30
  }
31
31
 
32
32
  function fail(errors: string[]): StepResult {
33
33
  return { ok: false, changedFiles: [], warnings: [], errors };
34
34
  }
35
35
 
36
+ function androidApplicationId(content: string): string | undefined {
37
+ const match = content.match(/^\s*applicationId\s+(?:=+\s*)?["']([^"']*)["']/m);
38
+ return match?.[1];
39
+ }
40
+
41
+ function androidResValues(content: string): Map<string, string> {
42
+ const out = new Map<string, string>();
43
+ const re = /resValue\s*\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*(['"])(.*?)\3\s*\)/g;
44
+ for (const match of content.matchAll(re)) {
45
+ out.set(match[2], `${match[1]}:${match[4]}`);
46
+ }
47
+ return out;
48
+ }
49
+
50
+ function androidConfigChangeLogs(before: string, after: string): string[] {
51
+ const logs: string[] = [];
52
+ const beforeAppId = androidApplicationId(before);
53
+ const afterAppId = androidApplicationId(after);
54
+ if (afterAppId !== undefined && beforeAppId !== afterAppId) {
55
+ logs.push(`[meetgames] Android applicationId ${beforeAppId === undefined ? "<missing>" : beforeAppId} -> ${afterAppId}`);
56
+ }
57
+
58
+ const beforeRes = androidResValues(before);
59
+ const afterRes = androidResValues(after);
60
+ for (const [key, next] of afterRes) {
61
+ const prev = beforeRes.get(key);
62
+ if (prev === undefined) {
63
+ logs.push(`[meetgames] Android resValue ${key} <missing> -> ${next}`);
64
+ } else if (prev !== next) {
65
+ logs.push(`[meetgames] Android resValue ${key} ${prev} -> ${next}`);
66
+ }
67
+ }
68
+ return logs;
69
+ }
70
+
36
71
  function readRootBuildGradle(projectRoot: string): { rel: string; abs: string } | null {
37
72
  const a = path.join(projectRoot, "build.gradle");
38
73
  if (fs.existsSync(a)) return { rel: "build.gradle", abs: a };
@@ -183,7 +218,7 @@ export const opHandlers: Record<string, OpHandler> = {
183
218
 
184
219
  store.write(modRel, modU.content);
185
220
  if (modU.changed) changed.push(modRel);
186
- return ok(changed, [...warnings, ...modU.warnings]);
221
+ return ok(changed, [...warnings, ...modU.warnings], androidConfigChangeLogs(modBefore, modU.content));
187
222
  },
188
223
 
189
224
  "gradle.insertRepositories": (ctx, store, args, _dry, _bc) => {