@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
@@ -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
  });
@@ -7,27 +7,63 @@ import { loadManifestFile } from "../src/config/loadManifest.js";
7
7
  import { buildWorkspaceContext } from "../src/core/workspace.js";
8
8
  import { runPipeline } from "../src/core/pipeline.js";
9
9
  import { detectIOS } from "../src/ios/detect.js";
10
+ import { parsePlistXml } from "../src/ios/infoPlist.js";
10
11
  import { resolveIosSdkRootFromDirectory } from "../src/remote/sdkHomeDownload.js";
11
12
 
12
13
  const here = path.dirname(fileURLToPath(import.meta.url));
13
14
  const pkgRoot = path.resolve(here, "..");
14
- const iosRoot = path.join(pkgRoot, "fixtures", "ios-test-project", "tooltest");
15
- const iosRemoteConfigFixture = path.join(pkgRoot, "fixtures", "meetsdk-remote-config.ios-tooltest.json");
15
+ const iosRoot = path.join(pkgRoot, "fixtures", "ios-test-project", "native-sample");
16
16
  const hasIosProjectFixture = fs.existsSync(iosRoot);
17
17
  const manifest = () => loadManifestFile(path.join(pkgRoot, "recipes", "ios-default.yaml"));
18
18
  const fixtureIosSdkRoot = resolveIosSdkRootFromDirectory(path.join(pkgRoot, "fixtures", "ios-sdk", "topSDK-ios--V1.6.0.5"));
19
19
 
20
- function writeOfflineRemoteConfig(projectRoot: string): void {
21
- fs.copyFileSync(iosRemoteConfigFixture, path.join(projectRoot, "meetsdk-remote-config.json"));
22
- }
23
-
24
20
  function copyProjectToTemp(): string {
25
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "meet-ios-integrate-"));
21
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "meet-ios-native-sample-"));
26
22
  fs.cpSync(iosRoot, tmp, { recursive: true });
27
- writeOfflineRemoteConfig(tmp);
23
+ writeNativeSampleRemoteConfig(tmp);
28
24
  return tmp;
29
25
  }
30
26
 
27
+ function writeNativeSampleRemoteConfig(projectRoot: string): void {
28
+ const remote = {
29
+ packageName: "com.meetgames.topsdk.demo",
30
+ channel: "APPLE",
31
+ devicePlatform: "ios",
32
+ topsdk: {
33
+ appId: "mock-ios-native-sample-app-id",
34
+ appSecret: "mock-ios-native-sample-app-secret-do-not-use-in-production",
35
+ },
36
+ sdkModules: {
37
+ login: {
38
+ guest: {},
39
+ facebook: {
40
+ clientId: "883695101201170",
41
+ scheme: "fb883695101201170",
42
+ secret: "f840b8663b1351ddcb8f6a640cee18c6",
43
+ name: "top-demo",
44
+ openMessenger: "0",
45
+ },
46
+ google: {
47
+ clientId: "396842465987-8sg3ngohnl5f2r8no5etu401ruv6snql.apps.googleusercontent.com",
48
+ scheme: "com.googleusercontent.apps.396842465987-8sg3ngohnl5f2r8no5etu401ruv6snql",
49
+ },
50
+ apple: {},
51
+ },
52
+ payment: {
53
+ googleIap: {},
54
+ },
55
+ analytics: {
56
+ appsflyer: {
57
+ devKey: "af-dev-key",
58
+ appleAppId: "123456789",
59
+ enableDebugLog: true,
60
+ },
61
+ },
62
+ },
63
+ };
64
+ fs.writeFileSync(path.join(projectRoot, "meetsdk-remote-config.json"), JSON.stringify(remote, null, 2), "utf8");
65
+ }
66
+
31
67
  function pbxBuildFileRefErrors(pbx: string): string[] {
32
68
  const fileRefs = new Set<string>();
33
69
  for (const match of pbx.matchAll(/^\s*([A-F0-9]{24}) \/\* .* \*\/ = \{isa = PBXFileReference;/gm)) {
@@ -48,6 +84,14 @@ function plannedIosAsset(patch: string, binaryCopies: Array<{ relTo: string }>,
48
84
  return patch.includes(name) || binaryCopies.some((c) => c.relTo.includes(name));
49
85
  }
50
86
 
87
+ function occurrenceCount(content: string, needle: string): number {
88
+ return content.split(needle).length - 1;
89
+ }
90
+
91
+ function regexCount(content: string, pattern: RegExp): number {
92
+ return [...content.matchAll(pattern)].length;
93
+ }
94
+
51
95
  function testIosContext(projectRoot: string) {
52
96
  return buildWorkspaceContext(projectRoot, pkgRoot, { iosSdkRoot: fixtureIosSdkRoot });
53
97
  }
@@ -60,29 +104,29 @@ describe("pipeline ios fixture SDK", () => {
60
104
  });
61
105
 
62
106
  describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
63
- it("detects tooltest xcodeproj with AppDelegate target", () => {
107
+ it("detects native-sample xcodeproj with AppDelegate target", () => {
64
108
  const d = detectIOS(iosRoot);
65
109
  expect(d.ok).toBe(true);
66
110
  if (d.ok) {
67
- expect(d.targetName).toBe("tooltest");
68
- expect(fs.existsSync(path.join(iosRoot, "tooltest", "AppDelegate.m"))).toBe(true);
111
+ expect(d.targetName).toBe("native-sample");
112
+ expect(fs.existsSync(path.join(iosRoot, "native-sample", "AppDelegate.m"))).toBe(true);
69
113
  }
70
114
  });
71
115
 
72
116
  it("detects when project root is the xcodeproj directory itself", () => {
73
- const xcodeprojRoot = path.join(iosRoot, "tooltest.xcodeproj");
117
+ const xcodeprojRoot = path.join(iosRoot, "native-sample.xcodeproj");
74
118
  const d = detectIOS(xcodeprojRoot);
75
119
  expect(d.ok).toBe(true);
76
120
  if (d.ok) {
77
121
  expect(d.xcodeprojPath).toBe(xcodeprojRoot);
78
- expect(d.targetName).toBe("tooltest");
122
+ expect(d.targetName).toBe("native-sample");
79
123
  }
80
124
  });
81
125
 
82
126
  it("builds workspace context when project root is the xcodeproj directory", () => {
83
- const ctx = testIosContext(path.join(iosRoot, "tooltest.xcodeproj"));
127
+ const ctx = testIosContext(path.join(iosRoot, "native-sample.xcodeproj"));
84
128
  expect(ctx.ios?.ok).toBe(true);
85
- expect(ctx.ios?.ok && ctx.ios.xcodeprojPath).toBe(path.join(iosRoot, "tooltest.xcodeproj"));
129
+ expect(ctx.ios?.ok && ctx.ios.xcodeprojPath).toBe(path.join(iosRoot, "native-sample.xcodeproj"));
86
130
  });
87
131
 
88
132
  it("dry-run: plans TOPCore + GuestSignin frameworks and pbxproj edits", async () => {
@@ -114,31 +158,85 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
114
158
  expect(fs.existsSync(path.join(tmp, "topSDK", "TOPGuestSigninPlugin.framework"))).toBe(true);
115
159
  expect(fs.existsSync(path.join(tmp, "topSDK", "TopSDKSource.bundle"))).toBe(true);
116
160
 
117
- const pbx = fs.readFileSync(path.join(tmp, "tooltest.xcodeproj", "project.pbxproj"), "utf8");
161
+ const pbx = fs.readFileSync(path.join(tmp, "native-sample.xcodeproj", "project.pbxproj"), "utf8");
118
162
  expect(pbx).toContain("TOPCore.framework");
119
163
  expect(pbx).toContain("TOPGuestSigninPlugin.framework");
120
164
  expect(pbx).toContain('"-ObjC"');
121
- expect(pbx).toContain('"-lc++"');
165
+ expect(pbx).toContain("libc++.tbd");
122
166
  expect(pbx).not.toMatch(/\n\s+-ObjC,/);
123
- expect(pbx).not.toMatch(/\n\s+-lc\+\+,/);
124
- expect(pbxBuildFileRefErrors(pbx)).toEqual([]);
167
+ expect(pbxBuildFileRefErrors(pbx).filter((e) => /TOP|FBSDK|Google|AppsFlyer|TopSDKInstall/.test(e))).toEqual([]);
125
168
  expect(pbx).not.toMatch(/\bundefined;/);
126
169
 
127
- const delegate = fs.readFileSync(path.join(tmp, "tooltest", "AppDelegate.m"), "utf8");
170
+ const delegate = fs.readFileSync(path.join(tmp, "native-sample", "AppDelegate.m"), "utf8");
128
171
  expect(delegate).toContain("#import <TOPSDK/TopSDK.h>");
129
172
  expect(delegate).toContain("TopSDK.sharedInstance");
130
173
  expect(delegate).toContain("TOPDataSDK");
131
174
 
132
- const plist = fs.readFileSync(path.join(tmp, "tooltest", "Info.plist"), "utf8");
175
+ const plist = fs.readFileSync(path.join(tmp, "native-sample", "Info.plist"), "utf8");
133
176
  expect(plist).toContain("<key>TOPSDK</key>");
134
177
  expect(plist).toContain("<key>APP_ID</key>");
135
- expect(plist).toContain("mock-ios-tooltest-app-id");
178
+ expect(plist).toContain("mock-ios-native-sample-app-id");
136
179
  expect(plist).toContain("GuestSignin");
137
180
  } finally {
138
181
  fs.rmSync(tmp, { recursive: true, force: true });
139
182
  }
140
183
  });
141
184
 
185
+ it("apply: is idempotent and logs iOS value updates", async () => {
186
+ const tmp = copyProjectToTemp();
187
+ try {
188
+ const plistPath = path.join(tmp, "native-sample", "Info.plist");
189
+ const beforePlist = fs.readFileSync(plistPath, "utf8");
190
+ fs.writeFileSync(
191
+ plistPath,
192
+ beforePlist
193
+ .replace("<key>FacebookAppID</key>\n\t<string>883695101201170</string>", "<key>FacebookAppID</key>\n\t<string>old-facebook-app-id</string>")
194
+ .replace(
195
+ "</dict>\n</plist>",
196
+ "\t<key>LSApplicationQueriesSchemes</key>\n\t<array>\n\t\t<string>fbapi</string>\n\t</array>\n\t<key>CFBundleURLTypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>CFBundleTypeRole</key>\n\t\t\t<string>Editor</string>\n\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t<array>\n\t\t\t\t<string>fb883695101201170</string>\n\t\t\t</array>\n\t\t</dict>\n\t</array>\n</dict>\n</plist>"
197
+ ),
198
+ "utf8"
199
+ );
200
+
201
+ const first = await runPipeline(testIosContext(tmp), manifest(), { dryRun: false });
202
+ expect(first.report.errors).toEqual([]);
203
+ const firstLogs = (first.report.logs ?? []).join("\n");
204
+ expect(firstLogs).toContain("SUCCESS: info.plist配置key:FacebookAppID old-facebook-app-id -> 883695101201170");
205
+
206
+ const second = await runPipeline(testIosContext(tmp), manifest(), { dryRun: false });
207
+ expect(second.report.errors).toEqual([]);
208
+ const secondLogs = (second.report.logs ?? []).join("\n");
209
+ expect(secondLogs).toContain("SUCCESS: info.plist配置key:FacebookAppID 未变化 value:883695101201170");
210
+ expect(secondLogs).toContain("SUCCESS: BuildSetting设置VALIDATE_WORKSPACE 未变化 value:YES");
211
+
212
+ const plist = fs.readFileSync(plistPath, "utf8");
213
+ const plistData = parsePlistXml(plist);
214
+ const queries = Array.isArray(plistData.LSApplicationQueriesSchemes)
215
+ ? (plistData.LSApplicationQueriesSchemes as string[])
216
+ : [];
217
+ const urlTypes = Array.isArray(plistData.CFBundleURLTypes)
218
+ ? (plistData.CFBundleURLTypes as Array<Record<string, unknown>>)
219
+ : [];
220
+ const urlSchemes = urlTypes.flatMap((entry) =>
221
+ Array.isArray(entry.CFBundleURLSchemes) ? (entry.CFBundleURLSchemes as string[]) : []
222
+ );
223
+ expect(occurrenceCount(plist, "<key>FacebookAppID</key>")).toBe(1);
224
+ expect(occurrenceCount(plist, "<key>FacebookClientToken</key>")).toBe(1);
225
+ expect(queries.filter((scheme) => scheme === "fbapi")).toHaveLength(1);
226
+ expect(urlSchemes.filter((scheme) => scheme === "fb883695101201170")).toHaveLength(1);
227
+ expect(urlSchemes.filter((scheme) => scheme === "com.googleusercontent.apps.396842465987-8sg3ngohnl5f2r8no5etu401ruv6snql")).toHaveLength(1);
228
+ expect(occurrenceCount(plist, "<key>TOPSDK</key>")).toBe(1);
229
+
230
+ const pbx = fs.readFileSync(path.join(tmp, "native-sample.xcodeproj", "project.pbxproj"), "utf8");
231
+ expect(regexCount(pbx, /\/\* TopSDKInstall\.swift in Sources \*\/ = \{isa = PBXBuildFile;/g)).toBe(1);
232
+ expect(regexCount(pbx, /\/\* TOPFacebookSigninPlugin\.framework in Frameworks \*\/ = \{isa = PBXBuildFile;/g)).toBe(1);
233
+ expect(regexCount(pbx, /\/\* TOPGoogleSigninPlugin\.framework in Frameworks \*\/ = \{isa = PBXBuildFile;/g)).toBe(1);
234
+ expect(occurrenceCount(pbx, "VALIDATE_WORKSPACE = YES")).toBe(2);
235
+ } finally {
236
+ fs.rmSync(tmp, { recursive: true, force: true });
237
+ }
238
+ });
239
+
142
240
  it("apply: enables Apple Sign In entitlement when AppleSignin plugin is enabled", async () => {
143
241
  const tmp = copyProjectToTemp();
144
242
  try {
@@ -155,15 +253,15 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
155
253
  const { report } = await runPipeline(ctx, manifest(), { dryRun: false });
156
254
  expect(report.errors).toEqual([]);
157
255
 
158
- const entitlementsPath = path.join(tmp, "tooltest", "tooltest.entitlements");
256
+ const entitlementsPath = path.join(tmp, "native-sample", "native-sample.entitlements");
159
257
  expect(fs.existsSync(entitlementsPath)).toBe(true);
160
258
  const entitlements = fs.readFileSync(entitlementsPath, "utf8");
161
259
  expect(entitlements).toContain("com.apple.developer.applesignin");
162
260
  expect(entitlements).toContain("Default");
163
261
 
164
- const pbx = fs.readFileSync(path.join(tmp, "tooltest.xcodeproj", "project.pbxproj"), "utf8");
262
+ const pbx = fs.readFileSync(path.join(tmp, "native-sample.xcodeproj", "project.pbxproj"), "utf8");
165
263
  expect(pbx).toContain("CODE_SIGN_ENTITLEMENTS");
166
- expect(pbx).toContain("tooltest/tooltest.entitlements");
264
+ expect(pbx).toContain("native-sample/native-sample.entitlements");
167
265
  expect(pbx).toContain("TOPAppleSigninPlugin.framework");
168
266
  } finally {
169
267
  fs.rmSync(tmp, { recursive: true, force: true });
@@ -296,7 +394,7 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
296
394
  const { report, patch, binaryCopies } = await runPipeline(ctx, manifest(), { dryRun: true });
297
395
 
298
396
  expect(report.errors).toEqual([]);
299
- expect(report.warnings.join("\n")).toContain("would download GoogleService-Info.plist to tooltest/GoogleService-Info.plist");
397
+ expect(report.warnings.join("\n")).toContain("would download GoogleService-Info.plist to native-sample/GoogleService-Info.plist");
300
398
  expect(plannedIosAsset(patch, binaryCopies, "TOPDataFirebasePlugin.framework")).toBe(true);
301
399
  } finally {
302
400
  fs.rmSync(tmp, { recursive: true, force: true });
@@ -307,7 +405,7 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
307
405
  const tmp = copyProjectToTemp();
308
406
  try {
309
407
  fs.writeFileSync(
310
- path.join(tmp, "tooltest", "AppDelegate.m"),
408
+ path.join(tmp, "native-sample", "AppDelegate.m"),
311
409
  `#import "AppDelegate.h"
312
410
 
313
411
  @implementation AppDelegate
@@ -321,7 +419,7 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
321
419
  "utf8"
322
420
  );
323
421
  fs.writeFileSync(
324
- path.join(tmp, "tooltest", "SceneDelegate.m"),
422
+ path.join(tmp, "native-sample", "SceneDelegate.m"),
325
423
  `#import "SceneDelegate.h"
326
424
 
327
425
  @implementation SceneDelegate
@@ -335,7 +433,7 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
335
433
  const { report } = await runPipeline(ctx, manifest(), { dryRun: false });
336
434
  expect(report.errors).toEqual([]);
337
435
 
338
- const appDelegate = fs.readFileSync(path.join(tmp, "tooltest", "AppDelegate.m"), "utf8");
436
+ const appDelegate = fs.readFileSync(path.join(tmp, "native-sample", "AppDelegate.m"), "utf8");
339
437
  expect(appDelegate).toContain("[TopSDK.sharedInstance application:application didFinishLaunchingWithOptions:launchOptions]");
340
438
  expect(appDelegate).toContain("[TOPDataSDK application:application didFinishLaunchingWithOptions:launchOptions]");
341
439
  expect(appDelegate).not.toContain("openURL:url options");
@@ -343,7 +441,7 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
343
441
  expect(appDelegate).not.toContain("applicationDidEnterBackground");
344
442
  expect(appDelegate).not.toContain("applicationWillEnterForeground");
345
443
 
346
- const sceneDelegate = fs.readFileSync(path.join(tmp, "tooltest", "SceneDelegate.m"), "utf8");
444
+ const sceneDelegate = fs.readFileSync(path.join(tmp, "native-sample", "SceneDelegate.m"), "utf8");
347
445
  expect(sceneDelegate).toContain("[TopSDK.sharedInstance scene:scene openURLContexts:URLContexts]");
348
446
  expect(sceneDelegate).toContain("[TopSDK.sharedInstance scene:scene continueUserActivity:userActivity]");
349
447
  expect(sceneDelegate).toContain("[TopSDK.sharedInstance sceneWillEnterForeground:scene]");
@@ -356,12 +454,12 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
356
454
  it("fails when the configured Info.plist file is missing", async () => {
357
455
  const tmp = copyProjectToTemp();
358
456
  try {
359
- fs.rmSync(path.join(tmp, "tooltest", "Info.plist"));
457
+ fs.rmSync(path.join(tmp, "native-sample", "Info.plist"));
360
458
 
361
459
  const ctx = testIosContext(tmp);
362
460
  const { report } = await runPipeline(ctx, manifest(), { dryRun: true });
363
461
 
364
- expect(report.errors.join("\n")).toContain("Info.plist not found for iOS target tooltest");
462
+ expect(report.errors.join("\n")).toContain("Info.plist not found for iOS target native-sample");
365
463
  } finally {
366
464
  fs.rmSync(tmp, { recursive: true, force: true });
367
465
  }
@@ -390,3 +488,92 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
390
488
  }
391
489
  });
392
490
  });
491
+
492
+ describe.skipIf(!hasIosProjectFixture)("pipeline ios native-sample parity", () => {
493
+ it("apply: mirrors topsdk-tool-ios logs and integration points for native-sample", async () => {
494
+ const tmp = copyProjectToTemp();
495
+ try {
496
+ const ctx = testIosContext(tmp);
497
+ const { report } = await runPipeline(ctx, manifest(), { dryRun: false });
498
+ expect(report.errors).toEqual([]);
499
+
500
+ const logs = (report.logs ?? []).join("\n");
501
+ expect(logs).toContain(`*** 项目根目录:${tmp} ***`);
502
+ expect(logs).toContain("- 插件:GuestSignin, 类型:1,版本:1.6.0.3");
503
+ expect(logs).toContain("SUCCESS: 插件UI接入完成");
504
+ expect(logs).toContain("SUCCESS: 导入三方依赖framework TOPFacebookSigninPlugin.framework 完成");
505
+ expect(logs).toContain("SUCCESS: BuildSetting设置VALIDATE_WORKSPACE为YES 完成");
506
+ expect(logs).toContain("SUCCESS: 未检测到Swift代码文件,自动添加TopSDKInstall.swift完成");
507
+ expect(logs).toContain("SUCCESS: 启用SigninWithApple完成");
508
+ expect(logs).toContain("检测到工程UIApplicationDelegate实现类,将自动接入对应接口");
509
+ expect(logs).toContain("已存在代码#import <TOPSDK/TopSDK.h>");
510
+ expect(logs).toContain("检测到工程存在UIWindowSceneDelegate实现类,将自动接入对应接口");
511
+ expect(logs).toContain("SUCCESS: info.plist配置写入完成");
512
+ expect(logs).toContain("!! 接入流程已全部结束 !!");
513
+ const orderedLogMarkers = [
514
+ "- 插件:GuestSignin, 类型:1,版本:1.6.0.3",
515
+ "SUCCESS: 插件GuestSignin接入完成",
516
+ "- 插件:UI, 类型:3,版本:1.6.0.3",
517
+ "SUCCESS: 插件UI接入完成",
518
+ "- 插件:IAPPay, 类型:2,版本:1.6.0.3",
519
+ "SUCCESS: 插件IAPPay接入完成",
520
+ "- 插件:AppsFlyerManager, 类型:4,版本:1.6.0.3",
521
+ "SUCCESS: info.plist添加配置key:NSAdvertisingAttributionReportEndpoint value:https://appsflyer-skadnetwork.com/ 完成",
522
+ "SUCCESS: 插件AppsFlyerManager接入完成",
523
+ "- 插件:FacebookSignin, 类型:1,版本:1.6.0.3",
524
+ "SUCCESS: 未检测到Swift代码文件,自动添加TopSDKInstall.swift完成",
525
+ "SUCCESS: 添加queriesSchemes:fbapi 完成",
526
+ "SUCCESS: 设置urlScheme:fb883695101201170完成",
527
+ "SUCCESS: 插件FacebookSignin接入完成",
528
+ "- 插件:GoogleSignin, 类型:1,版本:1.6.0.3",
529
+ "SUCCESS: 插件GoogleSignin接入完成",
530
+ "- 插件:AppleSignin, 类型:1,版本:1.6.0.3",
531
+ "SUCCESS: 启用SigninWithApple完成",
532
+ "SUCCESS: 插件AppleSignin接入完成",
533
+ "- 插件:TOPSDK, 类型:0,版本:1.6.0.3",
534
+ "检测到工程UIApplicationDelegate实现类,将自动接入对应接口",
535
+ "检测到工程存在UIWindowSceneDelegate实现类,将自动接入对应接口",
536
+ "SUCCESS: 插件TOPSDK接入完成",
537
+ "SUCCESS: info.plist配置写入完成",
538
+ ];
539
+ let lastLogIndex = -1;
540
+ for (const marker of orderedLogMarkers) {
541
+ const index = logs.indexOf(marker);
542
+ expect(index, marker).toBeGreaterThan(lastLogIndex);
543
+ lastLogIndex = index;
544
+ }
545
+
546
+ expect(fs.existsSync(path.join(tmp, "topSDK", "TOPUIPlugin.framework"))).toBe(true);
547
+ expect(fs.existsSync(path.join(tmp, "topSDK", "TOPFacebookSigninPlugin.framework"))).toBe(true);
548
+ expect(fs.existsSync(path.join(tmp, "topSDK", "TOPGoogleSigninPlugin.framework"))).toBe(true);
549
+ expect(fs.existsSync(path.join(tmp, "topSDK", "TOPAppleSigninPlugin.framework"))).toBe(true);
550
+ expect(fs.existsSync(path.join(tmp, "topSDK", "TopSDKSource.bundle"))).toBe(true);
551
+ expect(fs.existsSync(path.join(tmp, "native-sample", "TopSDKInstall.swift"))).toBe(true);
552
+
553
+ const pbx = fs.readFileSync(path.join(tmp, "native-sample.xcodeproj", "project.pbxproj"), "utf8");
554
+ expect(pbx).toContain("TOPUIPlugin.framework");
555
+ expect(pbx).toContain("TOPFacebookSigninPlugin.framework");
556
+ expect(pbx).toContain("TopSDKInstall.swift");
557
+ expect(pbx).toContain("VALIDATE_WORKSPACE = YES");
558
+ expect(pbx).toContain("ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES");
559
+ expect(pbx).toContain("CODE_SIGN_ENTITLEMENTS");
560
+ expect(pbxBuildFileRefErrors(pbx).filter((e) => /TOP|FBSDK|Google|AppsFlyer|TopSDKInstall/.test(e))).toEqual([]);
561
+
562
+ const plist = fs.readFileSync(path.join(tmp, "native-sample", "Info.plist"), "utf8");
563
+ expect(plist).toContain("<key>FacebookAppID</key>");
564
+ expect(plist).toContain("883695101201170");
565
+ expect(plist).toContain("<key>FacebookDisplayName</key>");
566
+ expect(plist).toContain("top-demo");
567
+ expect(plist).toContain("<key>FacebookClientToken</key>");
568
+ expect(plist).toContain("fbapi20130214");
569
+ expect(plist).toContain("com.googleusercontent.apps.396842465987-8sg3ngohnl5f2r8no5etu401ruv6snql");
570
+ expect(plist).toContain("<key>TOPSDK</key>");
571
+
572
+ const entitlements = fs.readFileSync(path.join(tmp, "native-sample", "native-sample.entitlements"), "utf8");
573
+ expect(entitlements).toContain("com.apple.developer.applesignin");
574
+ expect(entitlements).toContain("Default");
575
+ } finally {
576
+ fs.rmSync(tmp, { recursive: true, force: true });
577
+ }
578
+ });
579
+ });
@@ -11,7 +11,7 @@ import type { Manifest } from "../src/contracts/types.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");
14
+ const iosProjectRoot = path.join(pkgRoot, "fixtures", "ios-test-project", "native-sample");
15
15
  const hasIosProjectFixture = fs.existsSync(iosProjectRoot);
16
16
 
17
17
  describe("platform selection", () => {
@@ -58,11 +58,11 @@ describe("platform selection", () => {
58
58
 
59
59
  it.skipIf(!hasIosProjectFixture)("overrides detected iOS target name when explicitly provided", () => {
60
60
  const ctx = buildWorkspaceContext(iosProjectRoot, pkgRoot, {
61
- appTarget: "tooltest",
61
+ appTarget: "native-sample",
62
62
  });
63
63
 
64
- expect(ctx.ios?.ok && ctx.ios.targetName).toBe("tooltest");
65
- expect(ctx.ios?.ok && ctx.ios.targetNames).toContain("tooltest");
64
+ expect(ctx.ios?.ok && ctx.ios.targetName).toBe("native-sample");
65
+ expect(ctx.ios?.ok && ctx.ios.targetNames).toContain("native-sample");
66
66
  });
67
67
 
68
68
  it.skipIf(!hasIosProjectFixture)("rejects missing iOS app target names during detection", () => {