@leonxin/meetgames 0.1.13 → 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.
@@ -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,6 +51,29 @@ export interface IosIntegrateOptions {
50
51
 
51
52
  const IOS_FIREBASE_CONFIG_FILE = "GoogleService-Info.plist";
52
53
 
54
+ function logValue(value: unknown): string {
55
+ if (value === undefined) return "<missing>";
56
+ if (typeof value === "string") return value;
57
+ return JSON.stringify(value);
58
+ }
59
+
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
+
53
77
  function ok(changed: string[], warnings: string[] = [], logs: string[] = []): StepResult {
54
78
  return { ok: true, changedFiles: changed, logs, warnings, errors: [] };
55
79
  }
@@ -70,7 +94,6 @@ function applyPlugin(
70
94
  remoteSources: Map<string, string> = new Map()
71
95
  ): void {
72
96
  const { config, sourceDir } = loaded;
73
- const srcRoot = pbx.srcRoot;
74
97
 
75
98
  logs.push(`- 插件:${config.name}, 类型:${config.type},版本:${config.pluginVersion}`);
76
99
 
@@ -163,7 +186,13 @@ function applyPlugin(
163
186
 
164
187
  if (perform.includes("buildSettings") && config.buildSettings) {
165
188
  for (const [k, v] of Object.entries(config.buildSettings)) {
166
- 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
+ }
167
196
  logs.push(`SUCCESS: BuildSetting设置${k}为${v} 完成`);
168
197
  }
169
198
  }
@@ -174,8 +203,6 @@ function applyPlugin(
174
203
  logs.push(`SUCCESS: 添加other link flag:${flag} 完成`);
175
204
  }
176
205
  }
177
-
178
- logs.push(`SUCCESS: 插件${config.name}接入完成`);
179
206
  }
180
207
 
181
208
  function validateLoadedPluginResourcesForIos(loaded: LoadedPluginConfig): string[] {
@@ -325,7 +352,8 @@ function mergePlistParams(
325
352
  loaded: LoadedPluginConfig,
326
353
  channelConfig: Record<string, unknown>,
327
354
  perform: readonly PerformSetting[],
328
- logs: string[]
355
+ logs: string[],
356
+ dirtyDocs: Set<PlistDocument>
329
357
  ): void {
330
358
  if (!perform.includes("infoParams") && !perform.includes("urlScheme") && !perform.includes("queriesSchemes")) {
331
359
  return;
@@ -334,20 +362,31 @@ function mergePlistParams(
334
362
  if (perform.includes("infoParams") && loaded.config.infoParams) {
335
363
  for (const [key, raw] of Object.entries(loaded.config.infoParams)) {
336
364
  const value = applyChannelTemplateValue(raw, channelConfig);
337
- addPlistParam(doc.data, key, value);
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);
338
371
  logs.push(`SUCCESS: info.plist添加配置key:${key} value:${String(value)} 完成`);
339
372
  }
340
373
  }
341
374
  if (perform.includes("queriesSchemes") && loaded.config.queriesSchemes?.length) {
342
375
  for (const scheme of loaded.config.queriesSchemes) {
376
+ const before = cloneValue(doc.data.LSApplicationQueriesSchemes ?? []);
343
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);
344
380
  logs.push(`SUCCESS: 添加queriesSchemes:${scheme} 完成`);
345
381
  }
346
382
  }
347
383
  if (perform.includes("urlScheme") && loaded.config.urlScheme) {
348
384
  let scheme = applyChannelTemplate(loaded.config.urlScheme, channelConfig);
349
385
  if (scheme.endsWith("://")) scheme = scheme.replace("://", "");
386
+ const before = cloneValue(doc.data.CFBundleURLTypes ?? []);
350
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);
351
390
  logs.push(`SUCCESS: 设置urlScheme:${scheme}完成`);
352
391
  }
353
392
  }
@@ -378,6 +417,89 @@ function buildTopSdkPluginInfo(
378
417
  return { pluginsInfo, dataPluginsInfo };
379
418
  }
380
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
+
381
503
  export async function runIosIntegrateTopSdk(
382
504
  ctx: WorkspaceContext,
383
505
  store: TextFileStore,
@@ -444,6 +566,7 @@ export async function runIosIntegrateTopSdk(
444
566
  } catch (e) {
445
567
  return fail([e instanceof Error ? e.message : String(e)], warnings, logs);
446
568
  }
569
+ const dirtyPlistDocs = new Set<PlistDocument>();
447
570
 
448
571
  const firebaseDownload = await downloadIosFirebaseConfig(remote, pbx, dryRun, iosFirebaseConfigRelPath(plistDocs));
449
572
  warnings.push(...firebaseDownload.warnings);
@@ -466,112 +589,64 @@ export async function runIosIntegrateTopSdk(
466
589
  logs.push("SUCCESS: 未检测到Swift代码文件,自动添加TopSDKInstall.swift完成");
467
590
  }
468
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}接入完成`);
469
608
  }
470
609
  for (const loaded of coreConfigs) {
471
610
  applyPlugin(loaded, pbx, fm, channelConfig, perform, binaryCopies, dryRun, logs, firebaseDownload.remoteSources);
472
- }
473
-
474
- for (const loaded of [...coreConfigs, ...pluginConfigs]) {
475
- mergePlistParams(plistDocs, loaded, channelConfig, perform, logs);
611
+ mergePlistParams(plistDocs, loaded, channelConfig, perform, logs, dirtyPlistDocs);
476
612
  if (perform.includes("infoParams")) {
477
- 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
+ }
478
619
  }
620
+ if (executeAppDelegate) {
621
+ injectDelegateCodes([loaded], ctx, store, pbx, warnings, changed, logs);
622
+ }
623
+ logs.push(`SUCCESS: 插件${loaded.config.name}接入完成`);
479
624
  }
480
625
  for (const doc of plistDocs) {
481
626
  if (perform.includes("infoParams")) {
482
627
  const { pluginsInfo, dataPluginsInfo } = buildTopSdkPluginInfo(pluginConfigs, channelConfig);
483
- addPlistParam(doc.data, "TOPSDK", {
628
+ const before = doc.data.TOPSDK;
629
+ const nextTopSdk = {
484
630
  APP_ID: remote.topsdk.appId,
485
631
  Plugins: pluginsInfo,
486
632
  dataPlugins: dataPluginsInfo,
487
- });
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);
488
639
  }
489
640
  const rel = path.relative(ctx.projectRoot, path.join(pbx.srcRoot, doc.relPath)).split(path.sep).join("/");
490
- store.write(rel, buildPlistXml(doc.data));
491
- changed.add(rel);
492
- }
493
-
494
- if (perform.includes("infoParams") && pluginConfigs.some((p) => p.config.name === "AppleSignin")) {
495
- for (const rel of ensureAppleSignInEntitlement(store, ctx.projectRoot, pbx)) {
641
+ if (dirtyPlistDocs.has(doc)) {
642
+ store.write(rel, buildPlistXml(doc.data));
496
643
  changed.add(rel);
497
644
  }
498
- logs.push("SUCCESS: 启用SigninWithApple完成");
499
645
  }
500
646
 
501
647
  savePbxToStore(store, pbx);
502
648
  changed.add(pbx.rel);
503
649
 
504
- if (executeAppDelegate) {
505
- const sceneDelegateRelPaths: string[] = [];
506
- for (const abs of findSceneDelegateFiles(pbx.srcRoot)) {
507
- sceneDelegateRelPaths.push(path.relative(ctx.projectRoot, abs).split(path.sep).join("/"));
508
- }
509
- const hasUniqueSceneDelegate = sceneDelegateRelPaths.length === 1;
510
-
511
- const delegateRelPaths: string[] = [];
512
- for (const abs of findDelegateFiles(pbx.srcRoot)) {
513
- delegateRelPaths.push(path.relative(ctx.projectRoot, abs).split(path.sep).join("/"));
514
- }
515
- if (!delegateRelPaths.length) {
516
- warnings.push("no UIApplicationDelegate .m/.mm found; skipped AppDelegate injection (SwiftUI @main is not supported yet)");
517
- }
518
- if (delegateRelPaths.length > 1) {
519
- warnings.push(`multiple UIApplicationDelegate files found; skipped AppDelegate injection: ${delegateRelPaths.join(", ")}`);
520
- }
521
- for (const rel of delegateRelPaths.length === 1 ? delegateRelPaths : []) {
522
- const cu = CodeUtils.fromFile(path.join(ctx.projectRoot, rel));
523
- logs.push("检测到工程UIApplicationDelegate实现类,将自动接入对应接口");
524
- let okInject = true;
525
- for (const loaded of [...coreConfigs, ...pluginConfigs]) {
526
- for (const code of appDelegateCodesForLifecycle(loaded, hasUniqueSceneDelegate)) {
527
- const existed = cu.hasCode(code.content);
528
- if (code.type === "header") {
529
- if (!cu.addHeader(code.content)) okInject = false;
530
- } else if ((code.type === "method" || code.type === "code") && code.method) {
531
- if (!cu.addCodeToMethod(code.method, code.content, Boolean(code.addToReturn))) okInject = false;
532
- }
533
- if (okInject) {
534
- logs.push(
535
- `SUCCESS: 代码文件${path.join(ctx.projectRoot, rel)},${existed ? "已存在代码" : "添加代码"}${code.content}${existed ? "" : "完成"}`
536
- );
537
- }
538
- }
539
- }
540
- if (!okInject) warnings.push(`AppDelegate injection incomplete for ${rel}`);
541
- const before = store.read(rel);
542
- cu.applyToStore((c) => store.write(rel, c));
543
- if (store.read(rel) !== before) changed.add(rel);
544
- }
545
-
546
- if (sceneDelegateRelPaths.length > 1) {
547
- warnings.push(`multiple UIWindowSceneDelegate files found; skipped SceneDelegate injection: ${sceneDelegateRelPaths.join(", ")}`);
548
- }
549
- for (const rel of sceneDelegateRelPaths.length === 1 ? sceneDelegateRelPaths : []) {
550
- const cu = CodeUtils.fromFile(path.join(ctx.projectRoot, rel));
551
- logs.push("检测到工程存在UIWindowSceneDelegate实现类,将自动接入对应接口");
552
- let okInject = true;
553
- for (const loaded of [...coreConfigs, ...pluginConfigs]) {
554
- for (const code of loaded.config.sceneDelegateCodes ?? []) {
555
- const existed = cu.hasCode(code.content);
556
- if (code.type === "header") {
557
- if (!cu.addHeader(code.content)) okInject = false;
558
- } else if ((code.type === "method" || code.type === "code") && code.method) {
559
- if (!cu.addCodeToMethod(code.method, code.content, Boolean(code.addToReturn))) okInject = false;
560
- }
561
- if (okInject) {
562
- logs.push(
563
- `SUCCESS: 代码文件${path.join(ctx.projectRoot, rel)},${existed ? "已存在代码" : "添加代码"}${code.content}${existed ? "" : "完成"}`
564
- );
565
- }
566
- }
567
- }
568
- if (!okInject) warnings.push(`SceneDelegate injection incomplete for ${rel}`);
569
- const before = store.read(rel);
570
- cu.applyToStore((c) => store.write(rel, c));
571
- if (store.read(rel) !== before) changed.add(rel);
572
- }
573
- }
574
-
575
650
  logs.push("SUCCESS: info.plist配置写入完成");
576
651
  logs.push("!! 接入流程已全部结束 !!");
577
652
  return ok([...changed], warnings, logs);
@@ -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) => {
@@ -11,15 +11,55 @@ import { resolveIosSdkRootFromDirectory } from "../src/remote/sdkHomeDownload.js
11
11
 
12
12
  const pkgRoot = path.resolve(__dirname, "..");
13
13
  const androidLatestRoot = path.join(pkgRoot, "fixtures", "android-test-project", "android-latest-project");
14
- const iosProjectRoot = path.join(pkgRoot, "fixtures", "ios-test-project", "tooltest");
15
- const iosRemoteConfigFixture = path.join(pkgRoot, "fixtures", "meetsdk-remote-config.ios-tooltest.json");
14
+ const iosProjectRoot = path.join(pkgRoot, "fixtures", "ios-test-project", "native-sample");
16
15
  const hasIosProjectFixture = fs.existsSync(iosProjectRoot);
17
16
  const fixtureIosSdkRoot = resolveIosSdkRootFromDirectory(path.join(pkgRoot, "fixtures", "ios-sdk", "topSDK-ios--V1.6.0.5"));
18
17
 
19
18
  function copyIosProjectToTemp(): string {
20
19
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "meet-ios-doctor-"));
21
20
  fs.cpSync(iosProjectRoot, tmp, { recursive: true });
22
- fs.copyFileSync(iosRemoteConfigFixture, path.join(tmp, "meetsdk-remote-config.json"));
21
+ fs.writeFileSync(
22
+ path.join(tmp, "meetsdk-remote-config.json"),
23
+ JSON.stringify(
24
+ {
25
+ packageName: "com.meetgames.topsdk.demo",
26
+ channel: "APPLE",
27
+ devicePlatform: "ios",
28
+ topsdk: {
29
+ appId: "mock-ios-native-sample-app-id",
30
+ appSecret: "mock-ios-native-sample-app-secret-do-not-use-in-production",
31
+ },
32
+ sdkModules: {
33
+ login: {
34
+ guest: {},
35
+ facebook: {
36
+ clientId: "883695101201170",
37
+ scheme: "fb883695101201170",
38
+ secret: "f840b8663b1351ddcb8f6a640cee18c6",
39
+ name: "top-demo",
40
+ openMessenger: "0",
41
+ },
42
+ google: {
43
+ clientId: "396842465987-8sg3ngohnl5f2r8no5etu401ruv6snql.apps.googleusercontent.com",
44
+ scheme: "com.googleusercontent.apps.396842465987-8sg3ngohnl5f2r8no5etu401ruv6snql",
45
+ },
46
+ apple: {},
47
+ },
48
+ payment: { googleIap: {} },
49
+ analytics: {
50
+ appsflyer: {
51
+ devKey: "af-dev-key",
52
+ appleAppId: "123456789",
53
+ enableDebugLog: true,
54
+ },
55
+ },
56
+ },
57
+ },
58
+ null,
59
+ 2
60
+ ),
61
+ "utf8"
62
+ );
23
63
  return tmp;
24
64
  }
25
65
 
@@ -14,7 +14,7 @@ import { loadBuiltInMeetSdkDefaultConfig } from "../src/config/meetSdkDefaultCon
14
14
  const here = path.dirname(fileURLToPath(import.meta.url));
15
15
  const refPath = path.join(here, "..", "fixtures", "topsdk-config-reference.json");
16
16
  const offlineMockPath = path.join(here, "..", "fixtures", "meetsdk-remote-config.mock.json");
17
- const offlineIosMockPath = path.join(here, "..", "fixtures", "meetsdk-remote-config.ios-tooltest.json");
17
+ const offlineIosMockPath = path.join(here, "..", "fixtures", "meetsdk-remote-config.ios-native-sample.json");
18
18
  const downloadShapePath = path.join(here, "..", "fixtures", "meetsdk-remote-config.download-shape.json");
19
19
 
20
20
  const mapCtx = {
@@ -150,7 +150,7 @@ android {
150
150
  expect(modOut.content.indexOf(TOPSDK_PLUGIN_AUTO_START)).toBeLessThan(modOut.content.indexOf("dependencies"));
151
151
  });
152
152
 
153
- it("updates apply plugin by id instead of duplicating", () => {
153
+ it("keeps an existing apply plugin with the same id instead of duplicating or rewriting", () => {
154
154
  const modIn = `
155
155
  apply plugin: 'com.android.application'
156
156
  apply plugin: 'com.google.gms.google-services'
@@ -173,7 +173,7 @@ dependencies { }
173
173
  .split("\n")
174
174
  .filter((l) => l.includes("com.google.gms.google-services"));
175
175
  expect(pluginLines.length).toBe(1);
176
- expect(modOut.content).toContain(TOPSDK_PLUGIN_AUTO_START);
176
+ expect(modOut.content).not.toContain(TOPSDK_PLUGIN_AUTO_START);
177
177
  });
178
178
 
179
179
  it("updates classpath by group:artifact instead of duplicating", () => {
@@ -40,7 +40,10 @@ function testProjectRoot(...names: string[]): string {
40
40
  return path.join(androidFixtureRoot, names[0] ?? "");
41
41
  }
42
42
 
43
- async function runAndroidPipelineDryRun(projectRoot: string, opts?: { requirePackageNameInPatch?: boolean }) {
43
+ async function runAndroidPipelineDryRun(
44
+ projectRoot: string,
45
+ opts?: { requirePackageNameInPatch?: boolean; requireRemoteValuesInPatch?: boolean; allowEmptyPatch?: boolean }
46
+ ) {
44
47
  const manifest = loadManifestFile(path.join(recipeFixtureRoot, "android-default.fixture.yaml"));
45
48
  const cfg = JSON.parse(fs.readFileSync(path.join(projectRoot, "meetsdk-remote-config.json"), "utf8")) as {
46
49
  packageName: string;
@@ -51,20 +54,32 @@ async function runAndroidPipelineDryRun(projectRoot: string, opts?: { requirePac
51
54
  expect(ctx.android?.ok).toBe(true);
52
55
  const { report, patch, binaryCopies } = await runPipeline(ctx, manifest, { dryRun: true });
53
56
  expect(report.errors).toEqual([]);
54
- expect(patch.length).toBeGreaterThan(0);
57
+ if (!opts?.allowEmptyPatch) {
58
+ expect(patch.length).toBeGreaterThan(0);
59
+ }
55
60
  expect(binaryCopies).toEqual([]);
56
- expect(patch).toContain("TOPSDK REPO AUTO");
57
- expect(patch).toContain("TOPSDK AUTO");
61
+ if (!opts?.allowEmptyPatch || patch.length > 0) {
62
+ expect(patch).toContain("TOPSDK REPO AUTO");
63
+ expect(patch).toContain("TOPSDK AUTO");
64
+ }
58
65
  if (opts?.requirePackageNameInPatch !== false) {
59
66
  expect(patch).toContain(cfg.packageName);
60
67
  }
61
- expect(patch).toContain(cfg.topsdk.appId);
62
- expect(patch).toContain(cfg.channel);
63
- expect(patch).toContain("facebook_app_id");
64
- expect(patch).toContain("top_app_id");
68
+ if (opts?.requireRemoteValuesInPatch !== false) {
69
+ expect(patch).toContain(cfg.topsdk.appId);
70
+ expect(patch).toContain(cfg.channel);
71
+ expect(patch).toContain("facebook_app_id");
72
+ expect(patch).toContain("top_app_id");
73
+ }
65
74
  return { ctx, patch };
66
75
  }
67
76
 
77
+ function copyAndroidLatestToTemp(): string {
78
+ const tmp = fs.mkdtempSync(path.join(fs.realpathSync("/tmp"), "meet-android-idempotent-"));
79
+ fs.cpSync(androidLatestRoot, tmp, { recursive: true });
80
+ return tmp;
81
+ }
82
+
68
83
  describe("pipeline android fixture", () => {
69
84
  beforeEach(() => {
70
85
  stubSdkHomeVersion();
@@ -85,11 +100,50 @@ describe("pipeline android fixture", () => {
85
100
  const hasPowerRaidConfig = fs.existsSync(path.join(powerRaidRoot, "meetsdk-remote-config.json"));
86
101
 
87
102
  it.skipIf(!hasPowerRaidConfig)("dry-run on power-raid (:launcher)", async () => {
88
- const { ctx, patch } = await runAndroidPipelineDryRun(powerRaidRoot, { requirePackageNameInPatch: false });
103
+ const { ctx, patch } = await runAndroidPipelineDryRun(powerRaidRoot, {
104
+ requirePackageNameInPatch: false,
105
+ requireRemoteValuesInPatch: false,
106
+ allowEmptyPatch: true,
107
+ });
89
108
  expect(ctx.android?.ok && ctx.android.moduleName).toBe(":launcher");
90
- expect(patch).toContain("launcher/build.gradle");
91
109
  const addedLines = patch.split("\n").filter((line) => line.startsWith("+")).join("\n");
92
110
  expect(addedLines).not.toContain("//Firebase");
93
111
  expect(addedLines).not.toContain("//Appsflyer");
94
112
  });
113
+
114
+ it("apply is idempotent and logs Android value updates", async () => {
115
+ const tmp = copyAndroidLatestToTemp();
116
+ try {
117
+ const manifest = loadManifestFile(path.join(recipeFixtureRoot, "android-default.fixture.yaml"));
118
+ const buildGradlePath = path.join(tmp, "app", "build.gradle");
119
+ const beforeGradle = fs.readFileSync(buildGradlePath, "utf8");
120
+ fs.writeFileSync(
121
+ buildGradlePath,
122
+ beforeGradle.replace(
123
+ 'versionName "1.0"',
124
+ `versionName "1.0"
125
+ resValue('string', 'top_app_id', 'old-app-id')
126
+ resValue('string', 'facebook_app_id', 'old-facebook-id')`
127
+ ),
128
+ "utf8"
129
+ );
130
+
131
+ const ctx = buildWorkspaceContext(tmp, pkgRoot);
132
+ const first = await runPipeline(ctx, manifest, { dryRun: false });
133
+ expect(first.report.errors).toEqual([]);
134
+ const logs = (first.report.logs ?? []).join("\n");
135
+ expect(logs).toContain("Android applicationId com.example.myapplication -> com.meet.integrate.androidsample");
136
+ expect(logs).toContain("Android resValue top_app_id string:old-app-id -> string:mock-topsdk-app-id");
137
+ expect(logs).toContain("Android resValue facebook_app_id string:old-facebook-id -> string:0000000000000000");
138
+
139
+ const second = await runPipeline(buildWorkspaceContext(tmp, pkgRoot), manifest, { dryRun: false });
140
+ expect(second.report.errors).toEqual([]);
141
+ const after = fs.readFileSync(buildGradlePath, "utf8");
142
+ expect((after.match(/resValue\('string', 'top_app_id'/g) ?? []).length).toBe(1);
143
+ expect((after.match(/resValue\('string', 'facebook_app_id'/g) ?? []).length).toBe(1);
144
+ expect((after.match(/applicationId "com.meet.integrate.androidsample"/g) ?? []).length).toBe(1);
145
+ } finally {
146
+ fs.rmSync(tmp, { recursive: true, force: true });
147
+ }
148
+ });
95
149
  });