@leonxin/meetgames 0.1.13 → 0.1.15

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/config/meetsdk-ios.json +1 -1
  2. package/dist/android/manifest.d.ts.map +1 -1
  3. package/dist/android/manifest.js +9 -0
  4. package/dist/android/manifest.js.map +1 -1
  5. package/dist/android/meetSdkRemoteGradle.d.ts.map +1 -1
  6. package/dist/android/meetSdkRemoteGradle.js +126 -6
  7. package/dist/android/meetSdkRemoteGradle.js.map +1 -1
  8. package/dist/config/meetSdkRemoteConfig.d.ts +2 -0
  9. package/dist/config/meetSdkRemoteConfig.d.ts.map +1 -1
  10. package/dist/config/meetSdkRemoteConfig.js +14 -1
  11. package/dist/config/meetSdkRemoteConfig.js.map +1 -1
  12. package/dist/ios/channelConfig.d.ts.map +1 -1
  13. package/dist/ios/channelConfig.js +18 -1
  14. package/dist/ios/channelConfig.js.map +1 -1
  15. package/dist/ios/infoPlist.d.ts +0 -1
  16. package/dist/ios/infoPlist.d.ts.map +1 -1
  17. package/dist/ios/infoPlist.js +0 -5
  18. package/dist/ios/infoPlist.js.map +1 -1
  19. package/dist/ios/integrate.d.ts.map +1 -1
  20. package/dist/ios/integrate.js +172 -97
  21. package/dist/ios/integrate.js.map +1 -1
  22. package/dist/ios/pbxprojEditor.d.ts +1 -1
  23. package/dist/ios/pbxprojEditor.d.ts.map +1 -1
  24. package/dist/ios/pbxprojEditor.js +88 -15
  25. package/dist/ios/pbxprojEditor.js.map +1 -1
  26. package/dist/ops/fileStore.d.ts.map +1 -1
  27. package/dist/ops/fileStore.js +6 -5
  28. package/dist/ops/fileStore.js.map +1 -1
  29. package/dist/ops/handlers.d.ts.map +1 -1
  30. package/dist/ops/handlers.js +35 -3
  31. package/dist/ops/handlers.js.map +1 -1
  32. package/docs/API.md +1 -1
  33. package/docs/INTEGRATION.md +30 -2
  34. package/package.json +1 -1
  35. package/src/android/manifest.ts +11 -0
  36. package/src/android/meetSdkRemoteGradle.ts +125 -7
  37. package/src/config/meetSdkRemoteConfig.ts +13 -1
  38. package/src/ios/channelConfig.ts +18 -1
  39. package/src/ios/infoPlist.ts +0 -7
  40. package/src/ios/integrate.ts +177 -96
  41. package/src/ios/pbxprojEditor.ts +98 -16
  42. package/src/ops/fileStore.ts +7 -6
  43. package/src/ops/handlers.ts +38 -3
  44. package/tests/doctor.test.ts +43 -3
  45. package/tests/meetSdkRemoteConfig.test.ts +3 -2
  46. package/tests/meetSdkRemoteGradle.test.ts +2 -2
  47. package/tests/pipeline.android.test.ts +64 -10
  48. package/tests/pipeline.ios.test.ts +134 -43
  49. package/tests/platformSelection.test.ts +4 -4
@@ -20,7 +20,6 @@ import {
20
20
  addUrlScheme,
21
21
  buildPlistXml,
22
22
  parsePlistXml,
23
- setAppTransportSecurity,
24
23
  type PlistDocument,
25
24
  } from "./infoPlist.js";
26
25
  import {
@@ -31,6 +30,7 @@ import {
31
30
  addSystemLib,
32
31
  addThirdPartyFramework,
33
32
  findInfoPlistPathsFromPbx,
33
+ getTargetBuildSettings,
34
34
  loadPbxFromStore,
35
35
  savePbxToStore,
36
36
  setBuildSetting,
@@ -49,6 +49,34 @@ export interface IosIntegrateOptions {
49
49
  }
50
50
 
51
51
  const IOS_FIREBASE_CONFIG_FILE = "GoogleService-Info.plist";
52
+ const DEFAULT_IOS_BUILD_SETTINGS: Record<string, unknown> = {
53
+ FRAMEWORK_SEARCH_PATHS: ['"$(SRCROOT)/topSDK"', '"$(inherited)"', '"$(PROJECT_DIR)/topSDK"'],
54
+ LIBRARY_SEARCH_PATHS: '"$(SRCROOT)/topSDK"',
55
+ SWIFT_VERSION: "5.0",
56
+ };
57
+
58
+ function logValue(value: unknown): string {
59
+ if (value === undefined) return "<missing>";
60
+ if (typeof value === "string") return value;
61
+ return JSON.stringify(value);
62
+ }
63
+
64
+ function pushValueChangeLog(logs: string[], label: string, before: unknown, after: unknown): void {
65
+ if (valuesEqual(before, after)) {
66
+ logs.push(`SUCCESS: ${label} 未变化 value:${logValue(after)}`);
67
+ return;
68
+ }
69
+ logs.push(`SUCCESS: ${label} ${logValue(before)} -> ${logValue(after)}`);
70
+ }
71
+
72
+ function valuesEqual(before: unknown, after: unknown): boolean {
73
+ return JSON.stringify(before) === JSON.stringify(after);
74
+ }
75
+
76
+ function cloneValue(value: unknown): unknown {
77
+ if (value === undefined) return undefined;
78
+ return JSON.parse(JSON.stringify(value)) as unknown;
79
+ }
52
80
 
53
81
  function ok(changed: string[], warnings: string[] = [], logs: string[] = []): StepResult {
54
82
  return { ok: true, changedFiles: changed, logs, warnings, errors: [] };
@@ -70,7 +98,6 @@ function applyPlugin(
70
98
  remoteSources: Map<string, string> = new Map()
71
99
  ): void {
72
100
  const { config, sourceDir } = loaded;
73
- const srcRoot = pbx.srcRoot;
74
101
 
75
102
  logs.push(`- 插件:${config.name}, 类型:${config.type},版本:${config.pluginVersion}`);
76
103
 
@@ -163,7 +190,13 @@ function applyPlugin(
163
190
 
164
191
  if (perform.includes("buildSettings") && config.buildSettings) {
165
192
  for (const [k, v] of Object.entries(config.buildSettings)) {
166
- setBuildSetting(pbx, k, v);
193
+ const beforeValues = [...new Set(getTargetBuildSettings(pbx).map((settings) => settings[k]))];
194
+ if (beforeValues.length === 0 || beforeValues.some((before) => !valuesEqual(before, v))) {
195
+ setBuildSetting(pbx, k, v);
196
+ }
197
+ for (const before of beforeValues.length ? beforeValues : [undefined]) {
198
+ pushValueChangeLog(logs, `BuildSetting设置${k}`, before, v);
199
+ }
167
200
  logs.push(`SUCCESS: BuildSetting设置${k}为${v} 完成`);
168
201
  }
169
202
  }
@@ -174,8 +207,6 @@ function applyPlugin(
174
207
  logs.push(`SUCCESS: 添加other link flag:${flag} 完成`);
175
208
  }
176
209
  }
177
-
178
- logs.push(`SUCCESS: 插件${config.name}接入完成`);
179
210
  }
180
211
 
181
212
  function validateLoadedPluginResourcesForIos(loaded: LoadedPluginConfig): string[] {
@@ -266,14 +297,27 @@ function ensureTopSdkInstallSwift(
266
297
  pbx: PbxContext
267
298
  ): string | null {
268
299
  if (hasSwiftSource(pbx.srcRoot)) return null;
269
- const relFromSrc = path.join(pbx.targetName, "TopSDKInstall.swift").split(path.sep).join("/");
300
+ const relFromSrc = path.join("topSDK", "TopSDKInstall.swift").split(path.sep).join("/");
270
301
  const abs = path.join(pbx.srcRoot, relFromSrc);
271
302
  const relFromProject = path.relative(ctx.projectRoot, abs).split(path.sep).join("/");
272
- store.write(relFromProject, "import Foundation\n\n");
303
+ store.write(relFromProject, "");
273
304
  addSourceOrResourceFile(pbx, relFromSrc);
274
305
  return relFromProject;
275
306
  }
276
307
 
308
+ function applyDefaultIosBuildSettings(pbx: PbxContext, logs: string[]): void {
309
+ for (const [key, value] of Object.entries(DEFAULT_IOS_BUILD_SETTINGS)) {
310
+ const beforeValues = [...new Set(getTargetBuildSettings(pbx).map((settings) => settings[key]))];
311
+ if (beforeValues.length === 0 || beforeValues.some((before) => !valuesEqual(before, value))) {
312
+ setBuildSetting(pbx, key, value);
313
+ }
314
+ for (const before of beforeValues.length ? beforeValues : [undefined]) {
315
+ pushValueChangeLog(logs, `BuildSetting设置${key}`, before, value);
316
+ }
317
+ logs.push(`SUCCESS: BuildSetting设置${key}为${logValue(value)} 完成`);
318
+ }
319
+ }
320
+
277
321
  function appDelegateCodeShouldMoveToSceneDelegate(code: CodeConfig): boolean {
278
322
  if (code.type === "header") return false;
279
323
  const method = code.method ?? "";
@@ -325,7 +369,8 @@ function mergePlistParams(
325
369
  loaded: LoadedPluginConfig,
326
370
  channelConfig: Record<string, unknown>,
327
371
  perform: readonly PerformSetting[],
328
- logs: string[]
372
+ logs: string[],
373
+ dirtyDocs: Set<PlistDocument>
329
374
  ): void {
330
375
  if (!perform.includes("infoParams") && !perform.includes("urlScheme") && !perform.includes("queriesSchemes")) {
331
376
  return;
@@ -334,20 +379,31 @@ function mergePlistParams(
334
379
  if (perform.includes("infoParams") && loaded.config.infoParams) {
335
380
  for (const [key, raw] of Object.entries(loaded.config.infoParams)) {
336
381
  const value = applyChannelTemplateValue(raw, channelConfig);
337
- addPlistParam(doc.data, key, value);
382
+ const before = doc.data[key];
383
+ if (!valuesEqual(before, value)) {
384
+ addPlistParam(doc.data, key, value);
385
+ dirtyDocs.add(doc);
386
+ }
387
+ pushValueChangeLog(logs, `info.plist配置key:${key}`, before, value);
338
388
  logs.push(`SUCCESS: info.plist添加配置key:${key} value:${String(value)} 完成`);
339
389
  }
340
390
  }
341
391
  if (perform.includes("queriesSchemes") && loaded.config.queriesSchemes?.length) {
342
392
  for (const scheme of loaded.config.queriesSchemes) {
393
+ const before = cloneValue(doc.data.LSApplicationQueriesSchemes ?? []);
343
394
  addQueriesScheme(doc.data, scheme);
395
+ if (!valuesEqual(before, doc.data.LSApplicationQueriesSchemes)) dirtyDocs.add(doc);
396
+ pushValueChangeLog(logs, `info.plist queriesSchemes ${scheme}`, before, doc.data.LSApplicationQueriesSchemes);
344
397
  logs.push(`SUCCESS: 添加queriesSchemes:${scheme} 完成`);
345
398
  }
346
399
  }
347
400
  if (perform.includes("urlScheme") && loaded.config.urlScheme) {
348
401
  let scheme = applyChannelTemplate(loaded.config.urlScheme, channelConfig);
349
402
  if (scheme.endsWith("://")) scheme = scheme.replace("://", "");
403
+ const before = cloneValue(doc.data.CFBundleURLTypes ?? []);
350
404
  addUrlScheme(doc.data, scheme);
405
+ if (!valuesEqual(before, doc.data.CFBundleURLTypes)) dirtyDocs.add(doc);
406
+ pushValueChangeLog(logs, `info.plist URL Scheme ${scheme}`, before, doc.data.CFBundleURLTypes);
351
407
  logs.push(`SUCCESS: 设置urlScheme:${scheme}完成`);
352
408
  }
353
409
  }
@@ -367,7 +423,9 @@ function buildTopSdkPluginInfo(
367
423
  for (const [paramKey, rawValue] of Object.entries(loaded.config.pluginParams ?? {})) {
368
424
  params[paramKey] = applyChannelTemplate(String(rawValue), channelConfig);
369
425
  }
370
- info.params = params;
426
+ if (Object.keys(params).length > 0) {
427
+ info.params = params;
428
+ }
371
429
  }
372
430
  if (loaded.config.type === 4) {
373
431
  dataPluginsInfo.push(info);
@@ -378,6 +436,89 @@ function buildTopSdkPluginInfo(
378
436
  return { pluginsInfo, dataPluginsInfo };
379
437
  }
380
438
 
439
+ function injectDelegateCodes(
440
+ loadedConfigs: LoadedPluginConfig[],
441
+ ctx: WorkspaceContext,
442
+ store: TextFileStore,
443
+ pbx: PbxContext,
444
+ warnings: string[],
445
+ changed: Set<string>,
446
+ logs: string[]
447
+ ): void {
448
+ const hasAppDelegateCodes = loadedConfigs.some((loaded) => (loaded.config.appDelegateCodes ?? []).length > 0);
449
+ const hasSceneDelegateCodes = loadedConfigs.some((loaded) => (loaded.config.sceneDelegateCodes ?? []).length > 0);
450
+ if (!hasAppDelegateCodes && !hasSceneDelegateCodes) return;
451
+
452
+ const sceneDelegateRelPaths: string[] = [];
453
+ for (const abs of findSceneDelegateFiles(pbx.srcRoot)) {
454
+ sceneDelegateRelPaths.push(path.relative(ctx.projectRoot, abs).split(path.sep).join("/"));
455
+ }
456
+ const hasUniqueSceneDelegate = sceneDelegateRelPaths.length === 1;
457
+
458
+ const delegateRelPaths: string[] = [];
459
+ for (const abs of findDelegateFiles(pbx.srcRoot)) {
460
+ delegateRelPaths.push(path.relative(ctx.projectRoot, abs).split(path.sep).join("/"));
461
+ }
462
+ if (hasAppDelegateCodes && !delegateRelPaths.length) {
463
+ warnings.push("no UIApplicationDelegate .m/.mm found; skipped AppDelegate injection (SwiftUI @main is not supported yet)");
464
+ }
465
+ if (hasAppDelegateCodes && delegateRelPaths.length > 1) {
466
+ warnings.push(`multiple UIApplicationDelegate files found; skipped AppDelegate injection: ${delegateRelPaths.join(", ")}`);
467
+ }
468
+ for (const rel of hasAppDelegateCodes && delegateRelPaths.length === 1 ? delegateRelPaths : []) {
469
+ const cu = CodeUtils.fromFile(path.join(ctx.projectRoot, rel));
470
+ logs.push("检测到工程UIApplicationDelegate实现类,将自动接入对应接口");
471
+ let okInject = true;
472
+ for (const loaded of loadedConfigs) {
473
+ for (const code of appDelegateCodesForLifecycle(loaded, hasUniqueSceneDelegate)) {
474
+ const existed = cu.hasCode(code.content);
475
+ if (code.type === "header") {
476
+ if (!cu.addHeader(code.content)) okInject = false;
477
+ } else if ((code.type === "method" || code.type === "code") && code.method) {
478
+ if (!cu.addCodeToMethod(code.method, code.content, Boolean(code.addToReturn))) okInject = false;
479
+ }
480
+ if (okInject) {
481
+ logs.push(
482
+ `SUCCESS: 代码文件${path.join(ctx.projectRoot, rel)},${existed ? "已存在代码" : "添加代码"}${code.content}${existed ? "" : "完成"}`
483
+ );
484
+ }
485
+ }
486
+ }
487
+ if (!okInject) warnings.push(`AppDelegate injection incomplete for ${rel}`);
488
+ const before = store.read(rel);
489
+ cu.applyToStore((c) => store.write(rel, c));
490
+ if (store.read(rel) !== before) changed.add(rel);
491
+ }
492
+
493
+ if (hasSceneDelegateCodes && sceneDelegateRelPaths.length > 1) {
494
+ warnings.push(`multiple UIWindowSceneDelegate files found; skipped SceneDelegate injection: ${sceneDelegateRelPaths.join(", ")}`);
495
+ }
496
+ for (const rel of hasSceneDelegateCodes && sceneDelegateRelPaths.length === 1 ? sceneDelegateRelPaths : []) {
497
+ const cu = CodeUtils.fromFile(path.join(ctx.projectRoot, rel));
498
+ logs.push("检测到工程存在UIWindowSceneDelegate实现类,将自动接入对应接口");
499
+ let okInject = true;
500
+ for (const loaded of loadedConfigs) {
501
+ for (const code of loaded.config.sceneDelegateCodes ?? []) {
502
+ const existed = cu.hasCode(code.content);
503
+ if (code.type === "header") {
504
+ if (!cu.addHeader(code.content)) okInject = false;
505
+ } else if ((code.type === "method" || code.type === "code") && code.method) {
506
+ if (!cu.addCodeToMethod(code.method, code.content, Boolean(code.addToReturn))) okInject = false;
507
+ }
508
+ if (okInject) {
509
+ logs.push(
510
+ `SUCCESS: 代码文件${path.join(ctx.projectRoot, rel)},${existed ? "已存在代码" : "添加代码"}${code.content}${existed ? "" : "完成"}`
511
+ );
512
+ }
513
+ }
514
+ }
515
+ if (!okInject) warnings.push(`SceneDelegate injection incomplete for ${rel}`);
516
+ const before = store.read(rel);
517
+ cu.applyToStore((c) => store.write(rel, c));
518
+ if (store.read(rel) !== before) changed.add(rel);
519
+ }
520
+ }
521
+
381
522
  export async function runIosIntegrateTopSdk(
382
523
  ctx: WorkspaceContext,
383
524
  store: TextFileStore,
@@ -436,6 +577,9 @@ export async function runIosIntegrateTopSdk(
436
577
 
437
578
  const xcodeprojPath = ctx.ios.xcodeprojPath!;
438
579
  const pbx = await loadPbxFromStore(store, ctx.projectRoot, xcodeprojPath, targetName);
580
+ if (perform.includes("buildSettings")) {
581
+ applyDefaultIosBuildSettings(pbx, logs);
582
+ }
439
583
  const fm = new TopSdkFileManager(pbx.srcRoot);
440
584
 
441
585
  let plistDocs: PlistDocument[];
@@ -444,6 +588,7 @@ export async function runIosIntegrateTopSdk(
444
588
  } catch (e) {
445
589
  return fail([e instanceof Error ? e.message : String(e)], warnings, logs);
446
590
  }
591
+ const dirtyPlistDocs = new Set<PlistDocument>();
447
592
 
448
593
  const firebaseDownload = await downloadIosFirebaseConfig(remote, pbx, dryRun, iosFirebaseConfigRelPath(plistDocs));
449
594
  warnings.push(...firebaseDownload.warnings);
@@ -466,112 +611,48 @@ export async function runIosIntegrateTopSdk(
466
611
  logs.push("SUCCESS: 未检测到Swift代码文件,自动添加TopSDKInstall.swift完成");
467
612
  }
468
613
  }
614
+ mergePlistParams(plistDocs, loaded, channelConfig, perform, logs, dirtyPlistDocs);
615
+ if (perform.includes("infoParams") && loaded.config.name === "AppleSignin") {
616
+ for (const rel of ensureAppleSignInEntitlement(store, ctx.projectRoot, pbx)) {
617
+ changed.add(rel);
618
+ }
619
+ logs.push("SUCCESS: 启用SigninWithApple完成");
620
+ }
621
+ logs.push(`SUCCESS: 插件${loaded.config.name}接入完成`);
469
622
  }
470
623
  for (const loaded of coreConfigs) {
471
624
  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);
476
- if (perform.includes("infoParams")) {
477
- setAppTransportSecurity(plistDocs[0]?.data ?? {}, true);
625
+ mergePlistParams(plistDocs, loaded, channelConfig, perform, logs, dirtyPlistDocs);
626
+ if (executeAppDelegate) {
627
+ injectDelegateCodes([loaded], ctx, store, pbx, warnings, changed, logs);
478
628
  }
629
+ logs.push(`SUCCESS: 插件${loaded.config.name}接入完成`);
479
630
  }
480
631
  for (const doc of plistDocs) {
481
632
  if (perform.includes("infoParams")) {
482
633
  const { pluginsInfo, dataPluginsInfo } = buildTopSdkPluginInfo(pluginConfigs, channelConfig);
483
- addPlistParam(doc.data, "TOPSDK", {
634
+ const before = doc.data.TOPSDK;
635
+ const nextTopSdk = {
484
636
  APP_ID: remote.topsdk.appId,
485
637
  Plugins: pluginsInfo,
486
638
  dataPlugins: dataPluginsInfo,
487
- });
639
+ };
640
+ if (!valuesEqual(before, nextTopSdk)) {
641
+ addPlistParam(doc.data, "TOPSDK", nextTopSdk);
642
+ dirtyPlistDocs.add(doc);
643
+ }
644
+ pushValueChangeLog(logs, "info.plist配置key:TOPSDK", before, nextTopSdk);
488
645
  }
489
646
  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)) {
647
+ if (dirtyPlistDocs.has(doc)) {
648
+ store.write(rel, buildPlistXml(doc.data));
496
649
  changed.add(rel);
497
650
  }
498
- logs.push("SUCCESS: 启用SigninWithApple完成");
499
651
  }
500
652
 
501
653
  savePbxToStore(store, pbx);
502
654
  changed.add(pbx.rel);
503
655
 
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
656
  logs.push("SUCCESS: info.plist配置写入完成");
576
657
  logs.push("!! 接入流程已全部结束 !!");
577
658
  return ok([...changed], warnings, logs);
@@ -103,7 +103,14 @@ function sanitizePbxBuildSettings(proj: XcodeProject): void {
103
103
  continue;
104
104
  }
105
105
  if (Array.isArray(raw)) {
106
- settings[key] = raw.map((value) => (typeof value === "string" ? quotePbxStringIfNeeded(value) : value));
106
+ const normalized = raw.map((value) => (typeof value === "string" ? quotePbxStringIfNeeded(value) : value));
107
+ const seen = new Set<string>();
108
+ settings[key] = normalized.filter((value) => {
109
+ const marker = typeof value === "string" ? unquotePbxString(value) : JSON.stringify(value);
110
+ if (seen.has(marker)) return false;
111
+ seen.add(marker);
112
+ return true;
113
+ });
107
114
  }
108
115
  }
109
116
  }
@@ -162,12 +169,15 @@ function ensureResourcesBuildPhase(proj: XcodeProject, targetName: string): void
162
169
  export function addThirdPartyFramework(ctx: PbxContext, relPathFromSrcRoot: string, embed: boolean): void {
163
170
  const file = relPathFromSrcRoot.split(path.sep).join("/");
164
171
  const target = targetKey(ctx.proj, ctx.targetName);
165
- ctx.proj.addFramework(file, {
166
- customFramework: true,
167
- embed,
168
- sign: true,
169
- target,
170
- });
172
+ const basename = path.basename(file);
173
+ const fileRefUuid = ensureFileRef(ctx, file, frameworkFileType(file));
174
+ addBuildFileToPhase(ctx, ensureFrameworksBuildPhase(ctx, target), fileRefUuid, basename, "Frameworks", ["Weak"]);
175
+ if (embed) {
176
+ addBuildFileToPhase(ctx, ensureCopyFilesBuildPhase(ctx, target), fileRefUuid, basename, "Embed Frameworks", [
177
+ "CodeSignOnCopy",
178
+ "RemoveHeadersOnCopy",
179
+ ]);
180
+ }
171
181
  }
172
182
 
173
183
  export function addCopyFile(ctx: PbxContext, relPathFromSrcRoot: string): void {
@@ -202,13 +212,81 @@ function findFileRefUuid(ctx: PbxContext, file: string): string | null {
202
212
  return null;
203
213
  }
204
214
 
215
+ function frameworkFileType(file: string): string {
216
+ return file.endsWith(".xcframework") ? "wrapper.xcframework" : "wrapper.framework";
217
+ }
218
+
219
+ function ensureFileRef(ctx: PbxContext, file: string, lastKnownFileType: string): string {
220
+ const basename = path.basename(file);
221
+ const existing = findFileRefUuid(ctx, file);
222
+ const fileRefSection = objectSection(ctx.proj, "PBXFileReference");
223
+ if (existing) {
224
+ const ref = fileRefSection[existing];
225
+ if (ref && typeof ref === "object") {
226
+ (ref as Record<string, unknown>).lastKnownFileType = lastKnownFileType;
227
+ }
228
+ return existing;
229
+ }
230
+
231
+ const uuid = ctx.proj.generateUuid();
232
+ fileRefSection[uuid] = {
233
+ isa: "PBXFileReference",
234
+ name: `"${basename}"`,
235
+ path: `"${file}"`,
236
+ sourceTree: '"<group>"',
237
+ lastKnownFileType,
238
+ includeInIndex: 0,
239
+ };
240
+ fileRefSection[`${uuid}_comment`] = basename;
241
+ return uuid;
242
+ }
243
+
244
+ function ensureFrameworksBuildPhase(ctx: PbxContext, target: string): Record<string, unknown> {
245
+ try {
246
+ const phase = ctx.proj.pbxFrameworksBuildPhaseObj(target) as Record<string, unknown>;
247
+ phase.files = (phase.files as unknown[]) ?? [];
248
+ return phase;
249
+ } catch {
250
+ ctx.proj.addBuildPhase([], "PBXFrameworksBuildPhase", "Frameworks", target);
251
+ const phase = ctx.proj.pbxFrameworksBuildPhaseObj(target) as Record<string, unknown>;
252
+ phase.files = (phase.files as unknown[]) ?? [];
253
+ return phase;
254
+ }
255
+ }
256
+
257
+ function addBuildFileToPhase(
258
+ ctx: PbxContext,
259
+ phase: Record<string, unknown>,
260
+ fileRefUuid: string,
261
+ basename: string,
262
+ phaseName: string,
263
+ attributes?: string[]
264
+ ): void {
265
+ const files = (phase.files ??= []) as Array<{ value?: string; comment?: string }>;
266
+ if (files.some((file) => file.comment === `${basename} in ${phaseName}`)) return;
267
+
268
+ const buildUuid = ctx.proj.generateUuid();
269
+ const buildFileSection = objectSection(ctx.proj, "PBXBuildFile");
270
+ const buildFile: Record<string, unknown> = {
271
+ isa: "PBXBuildFile",
272
+ fileRef: fileRefUuid,
273
+ fileRef_comment: basename,
274
+ };
275
+ if (attributes?.length) {
276
+ buildFile.settings = { ATTRIBUTES: attributes };
277
+ }
278
+ buildFileSection[buildUuid] = buildFile;
279
+ buildFileSection[`${buildUuid}_comment`] = `${basename} in ${phaseName}`;
280
+ files.push({ value: buildUuid, comment: `${basename} in ${phaseName}` });
281
+ }
282
+
205
283
  function ensureCopyFilesBuildPhase(ctx: PbxContext, target: string): Record<string, unknown> {
206
284
  const section = objectSection(ctx.proj, "PBXCopyFilesBuildPhase");
207
285
  for (const [uuid, raw] of Object.entries(section)) {
208
286
  if (uuid.endsWith("_comment") || !raw || typeof raw !== "object") continue;
209
287
  const phase = raw as Record<string, unknown>;
210
288
  const name = String(phase.name ?? "").replace(/^"|"$/g, "");
211
- if (name === "Copy Files" || section[`${uuid}_comment`] === "Copy Files") {
289
+ if (name === "Embed Frameworks" || section[`${uuid}_comment`] === "Embed Frameworks") {
212
290
  phase.files = (phase.files as unknown[]) ?? [];
213
291
  return phase;
214
292
  }
@@ -221,15 +299,15 @@ function ensureCopyFilesBuildPhase(ctx: PbxContext, target: string): Record<stri
221
299
  dstPath: '""',
222
300
  dstSubfolderSpec: 10,
223
301
  files: [],
224
- name: '"Copy Files"',
302
+ name: '"Embed Frameworks"',
225
303
  runOnlyForDeploymentPostprocessing: 0,
226
304
  };
227
305
  section[uuid] = phase;
228
- section[`${uuid}_comment`] = "Copy Files";
306
+ section[`${uuid}_comment`] = "Embed Frameworks";
229
307
  const native = ctx.proj.pbxNativeTargetSection?.()[target] as { buildPhases?: Array<{ value: string; comment: string }> };
230
308
  native.buildPhases ??= [];
231
309
  if (!native.buildPhases.some((p) => p.value === uuid)) {
232
- native.buildPhases.push({ value: uuid, comment: "Copy Files" });
310
+ native.buildPhases.push({ value: uuid, comment: "Embed Frameworks" });
233
311
  }
234
312
  return phase;
235
313
  }
@@ -289,9 +367,13 @@ export function addSourceOrResourceFile(ctx: PbxContext, relPathFromSrcRoot: str
289
367
  const target = targetKey(ctx.proj, ctx.targetName);
290
368
  if (file.endsWith(".h")) {
291
369
  ctx.proj.addHeaderFile(file, { target });
292
- } else if (file.endsWith(".m") || file.endsWith(".mm") || file.endsWith(".swift")) {
370
+ } else if (file.endsWith(".m") || file.endsWith(".mm") || file.endsWith(".swift") || file.endsWith(".xcdatamodeld")) {
293
371
  try {
294
- ctx.proj.addSourceFile(file, { target });
372
+ if (file.endsWith(".xcdatamodeld")) {
373
+ addSourceFileManually(ctx, file, "wrapper.xcdatamodel");
374
+ } else {
375
+ ctx.proj.addSourceFile(file, { target });
376
+ }
295
377
  } catch (e) {
296
378
  if (!String(e instanceof Error ? e.message : e).includes("path")) throw e;
297
379
  addSourceFileManually(ctx, file);
@@ -327,7 +409,7 @@ function sourceFileType(file: string): string {
327
409
  return "sourcecode.c.objc";
328
410
  }
329
411
 
330
- function addSourceFileManually(ctx: PbxContext, file: string): void {
412
+ function addSourceFileManually(ctx: PbxContext, file: string, lastKnownFileType?: string): void {
331
413
  const basename = path.basename(file);
332
414
  ensureSourcesBuildPhase(ctx.proj, ctx.targetName);
333
415
  const sources = ctx.proj.pbxSourcesBuildPhaseObj(targetKey(ctx.proj, ctx.targetName));
@@ -344,7 +426,7 @@ function addSourceFileManually(ctx: PbxContext, file: string): void {
344
426
  name: `"${basename}"`,
345
427
  path: `"${file}"`,
346
428
  sourceTree: '"<group>"',
347
- lastKnownFileType: sourceFileType(file),
429
+ lastKnownFileType: lastKnownFileType ?? sourceFileType(file),
348
430
  };
349
431
  fileRefSection[`${fileRefUuid}_comment`] = basename;
350
432
  buildFileSection[buildUuid] = {
@@ -394,7 +476,7 @@ function addResourceFileManually(ctx: PbxContext, file: string, lastKnownFileTyp
394
476
  files.push({ value: buildUuid, comment: `${basename} in Resources` });
395
477
  }
396
478
 
397
- export function setBuildSetting(ctx: PbxContext, key: string, value: string): void {
479
+ export function setBuildSetting(ctx: PbxContext, key: string, value: unknown): void {
398
480
  const uuid = targetUuid(ctx.proj, ctx.targetName);
399
481
  if (!uuid) return;
400
482
  const native = ctx.proj.pbxNativeTargetSection?.()[uuid] as { buildConfigurationList?: string };
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  /** Tracks text file originals and pending content for dry-run / apply */
5
5
  export class TextFileStore {
6
6
  private readonly projectRoot: string;
7
- private readonly map = new Map<string, { original: string; current: string }>();
7
+ private readonly map = new Map<string, { original: string; current: string; existed: boolean }>();
8
8
 
9
9
  constructor(projectRoot: string) {
10
10
  this.projectRoot = projectRoot;
@@ -20,7 +20,7 @@ export class TextFileStore {
20
20
  const abs = path.join(this.projectRoot, rel);
21
21
  if (!fs.existsSync(abs)) return "";
22
22
  const content = fs.readFileSync(abs, "utf8");
23
- this.map.set(rel, { original: content, current: content });
23
+ this.map.set(rel, { original: content, current: content, existed: true });
24
24
  return content;
25
25
  }
26
26
 
@@ -32,22 +32,23 @@ export class TextFileStore {
32
32
  return;
33
33
  }
34
34
  let original = "";
35
- if (fs.existsSync(abs)) original = fs.readFileSync(abs, "utf8");
36
- this.map.set(rel, { original, current: content });
35
+ const existed = fs.existsSync(abs);
36
+ if (existed) original = fs.readFileSync(abs, "utf8");
37
+ this.map.set(rel, { original, current: content, existed });
37
38
  }
38
39
 
39
40
  /** Files where current !== original */
40
41
  changedEntries(): Array<{ rel: string; original: string; current: string }> {
41
42
  const out: Array<{ rel: string; original: string; current: string }> = [];
42
43
  for (const [rel, v] of this.map) {
43
- if (v.current !== v.original) out.push({ rel, original: v.original, current: v.current });
44
+ if (!v.existed || v.current !== v.original) out.push({ rel, original: v.original, current: v.current });
44
45
  }
45
46
  return out;
46
47
  }
47
48
 
48
49
  flushToDisk(): void {
49
50
  for (const [rel, v] of this.map) {
50
- if (v.current === v.original) continue;
51
+ if (v.existed && v.current === v.original) continue;
51
52
  const abs = path.join(this.projectRoot, rel);
52
53
  fs.mkdirSync(path.dirname(abs), { recursive: true });
53
54
  fs.writeFileSync(abs, v.current, "utf8");