@leonxin/meetgames 0.1.16 → 0.1.18

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.
@@ -2,12 +2,33 @@ import { spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { syncMeetSdkIosVersionToConfig } from "../config/meetSdkIosConfig.js";
5
+ import { hasSdkModuleKey, type MeetSdkRemoteConfig } from "../config/meetSdkRemoteConfig.js";
5
6
  import { resolveIosSdkCacheLayout, withCacheLock, writeIosSdkCacheMetadata } from "../cache.js";
6
7
 
7
8
  export const defaultSdkHomeApiBaseUrl = "https://business-api.meetgames.com";
8
9
  export const DEFAULT_IOS_SDK_PLUGINS = ["guest", "facebook", "google", "apple", "apple_pay", "dataAppsFlyer"] as const;
9
10
  export const DEFAULT_IOS_SDK_PACKAGE_TYPE = "native";
10
11
 
12
+ const IOS_SDK_DOWNLOAD_PLUGIN_BY_SUBKEY: Record<string, string> = {
13
+ guest: "guest",
14
+ email: "email",
15
+ facebook: "facebook",
16
+ google: "google",
17
+ snapchat: "snapchat",
18
+ discord: "discord",
19
+ line: "line",
20
+ naver: "naver",
21
+ kakao: "kakao",
22
+ tiktok: "tiktok",
23
+ apple: "apple",
24
+ googleIap: "google_pay",
25
+ appleIap: "apple_pay",
26
+ appsflyer: "dataAppsFlyer",
27
+ firebase: "dataFirebase",
28
+ adjust: "dataAdjust",
29
+ facebookdata: "dataFacebook",
30
+ };
31
+
11
32
  const SDK_HOME_VERSION_PATH = "sdk/home/version";
12
33
  const SDK_HOME_DOWNLOAD_URL_PATH = "sdk/home/sdk-download/getDownLoadUrl";
13
34
 
@@ -33,9 +54,9 @@ export interface SdkHomeDownloadUrlRequest {
33
54
 
34
55
  export interface DownloadIosSdkOptions {
35
56
  baseUrl?: string;
36
- version?: string;
37
57
  plugins?: readonly string[] | string;
38
58
  packageType?: string;
59
+ cacheRoot?: string;
39
60
  signal?: AbortSignal;
40
61
  }
41
62
 
@@ -43,13 +64,14 @@ export interface DownloadIosSdkResult {
43
64
  version: string;
44
65
  versionDate: string;
45
66
  versionUrl: string;
46
- downloadApiUrl: string;
47
- sdkZipUrl: string;
67
+ downloadApiUrl?: string;
68
+ sdkZipUrl?: string;
48
69
  zipPath: string;
49
70
  extractDir: string;
50
71
  resolvedSdkRoot: string;
51
72
  iosConfigPath: string;
52
73
  cacheMetadataPath: string;
74
+ downloaded: boolean;
53
75
  }
54
76
 
55
77
  function isRecord(value: unknown): value is JsonRecord {
@@ -73,6 +95,20 @@ function pluginsToList(plugins: readonly string[] | string | undefined): string[
73
95
  .filter(Boolean);
74
96
  }
75
97
 
98
+ export function resolveIosSdkDownloadPluginsFromRemoteConfig(config: MeetSdkRemoteConfig): string[] {
99
+ const plugins: string[] = [];
100
+ for (const bucket of Object.values(config.sdkModules)) {
101
+ if (!bucket || typeof bucket !== "object") continue;
102
+ const scopeRecord = bucket as Record<string, unknown>;
103
+ for (const subKey of Object.keys(scopeRecord)) {
104
+ if (!hasSdkModuleKey(scopeRecord, subKey)) continue;
105
+ const plugin = IOS_SDK_DOWNLOAD_PLUGIN_BY_SUBKEY[subKey];
106
+ if (plugin) plugins.push(plugin);
107
+ }
108
+ }
109
+ return [...new Set(plugins)];
110
+ }
111
+
76
112
  export function buildSdkHomeVersionUrl(baseUrl: string): string {
77
113
  return joinBaseAndPath(baseUrl, SDK_HOME_VERSION_PATH).toString();
78
114
  }
@@ -188,6 +224,68 @@ export function resolveIosSdkRootFromDirectory(bundledDir: string): string {
188
224
  throw new Error(`iOS SDK layout invalid under ${bundledDir}: need sdk/ + plugins/ at top level or under one version directory.`);
189
225
  }
190
226
 
227
+ function tryResolveIosSdkRootFromDirectory(bundledDir: string): string | null {
228
+ try {
229
+ return resolveIosSdkRootFromDirectory(bundledDir);
230
+ } catch {
231
+ return null;
232
+ }
233
+ }
234
+
235
+ function compareSdkVersions(a: string, b: string): number {
236
+ const left = a.split(/[.-]/);
237
+ const right = b.split(/[.-]/);
238
+ const len = Math.max(left.length, right.length);
239
+ for (let i = 0; i < len; i += 1) {
240
+ const l = left[i] ?? "0";
241
+ const r = right[i] ?? "0";
242
+ const ln = /^\d+$/.test(l) ? Number(l) : null;
243
+ const rn = /^\d+$/.test(r) ? Number(r) : null;
244
+ if (ln !== null && rn !== null) {
245
+ if (ln !== rn) return ln - rn;
246
+ continue;
247
+ }
248
+ const cmp = l.localeCompare(r);
249
+ if (cmp !== 0) return cmp;
250
+ }
251
+ return 0;
252
+ }
253
+
254
+ function findReusableCachedIosSdk(params: {
255
+ cacheRoot?: string;
256
+ packageType: string;
257
+ plugins: readonly string[];
258
+ serverVersion: string;
259
+ }): { version: string; layout: ReturnType<typeof resolveIosSdkCacheLayout>; resolvedSdkRoot: string } | null {
260
+ const probe = resolveIosSdkCacheLayout({
261
+ version: "__probe__",
262
+ packageType: params.packageType,
263
+ plugins: params.plugins,
264
+ cacheRoot: params.cacheRoot,
265
+ });
266
+ const packageDir = path.dirname(path.dirname(probe.baseDir));
267
+ if (!fs.existsSync(packageDir)) return null;
268
+
269
+ let best: { version: string; layout: ReturnType<typeof resolveIosSdkCacheLayout>; resolvedSdkRoot: string } | null = null;
270
+ for (const ent of fs.readdirSync(packageDir, { withFileTypes: true })) {
271
+ if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
272
+ const comparedToServer = compareSdkVersions(ent.name, params.serverVersion);
273
+ if (comparedToServer < 0) continue;
274
+ const layout = resolveIosSdkCacheLayout({
275
+ version: ent.name,
276
+ packageType: params.packageType,
277
+ plugins: params.plugins,
278
+ cacheRoot: params.cacheRoot,
279
+ });
280
+ const resolvedSdkRoot = tryResolveIosSdkRootFromDirectory(layout.extractDir);
281
+ if (!resolvedSdkRoot) continue;
282
+ if (!best || compareSdkVersions(ent.name, best.version) > 0) {
283
+ best = { version: ent.name, layout, resolvedSdkRoot };
284
+ }
285
+ }
286
+ return best;
287
+ }
288
+
191
289
  export async function downloadBinaryFile(url: string, absPath: string, signal?: AbortSignal): Promise<void> {
192
290
  let res: Response;
193
291
  try {
@@ -236,25 +334,39 @@ export async function downloadIosSdkToBundled(
236
334
  const baseUrl = options.baseUrl || defaultSdkHomeApiBaseUrl;
237
335
  const packageType = options.packageType || DEFAULT_IOS_SDK_PACKAGE_TYPE;
238
336
  const plugins = pluginsToList(options.plugins);
239
- const versionResult = options.version
240
- ? { url: buildSdkHomeVersionUrl(baseUrl), ios: { ver: options.version, date: "" } }
241
- : await fetchSdkHomeIosVersion({ baseUrl, signal: options.signal });
242
- const version = versionResult.ios.ver;
243
- const downloadResult = await fetchSdkHomeIosDownloadUrl({
244
- baseUrl,
245
- version,
246
- plugins,
337
+ const versionResult = await fetchSdkHomeIosVersion({ baseUrl, signal: options.signal });
338
+ const serverVersion = versionResult.ios.ver;
339
+ const reusableCache = findReusableCachedIosSdk({
340
+ cacheRoot: options.cacheRoot,
247
341
  packageType,
248
- signal: options.signal,
342
+ plugins,
343
+ serverVersion,
249
344
  });
250
- const cacheLayout = resolveIosSdkCacheLayout({ version, packageType, plugins });
345
+ const version = reusableCache?.version ?? serverVersion;
346
+ const cacheLayout =
347
+ reusableCache?.layout ?? resolveIosSdkCacheLayout({ version, packageType, plugins, cacheRoot: options.cacheRoot });
251
348
  const extractDir = cacheLayout.extractDir;
252
349
  const zipPath = cacheLayout.zipPath;
350
+ let downloadApiUrl: string | undefined;
351
+ let sdkZipUrl: string | undefined;
352
+ let downloaded = false;
253
353
 
254
354
  const downloadAndExtract = async (): Promise<void> => {
255
- await downloadBinaryFile(downloadResult.sdkZipUrl, zipPath, options.signal);
355
+ const cachedRoot = tryResolveIosSdkRootFromDirectory(extractDir);
356
+ if (cachedRoot) return;
357
+ const fetchedDownload = await fetchSdkHomeIosDownloadUrl({
358
+ baseUrl,
359
+ version,
360
+ plugins,
361
+ packageType,
362
+ signal: options.signal,
363
+ });
364
+ downloadApiUrl = fetchedDownload.url;
365
+ sdkZipUrl = fetchedDownload.sdkZipUrl;
366
+ await downloadBinaryFile(fetchedDownload.sdkZipUrl, zipPath, options.signal);
256
367
  fs.rmSync(extractDir, { recursive: true, force: true });
257
368
  await extractZip(zipPath, extractDir);
369
+ downloaded = true;
258
370
  };
259
371
  await withCacheLock(cacheLayout.lockDir, downloadAndExtract);
260
372
  const iosConfigPath = syncMeetSdkIosVersionToConfig({
@@ -266,30 +378,33 @@ export async function downloadIosSdkToBundled(
266
378
  });
267
379
  const resolvedSdkRoot = resolveIosSdkRootFromDirectory(extractDir);
268
380
  const cacheMetadataPath = cacheLayout.metadataPath;
269
- writeIosSdkCacheMetadata({
270
- metadataPath: cacheMetadataPath,
271
- version,
272
- date: versionResult.ios.date,
273
- packageType,
274
- plugins,
275
- versionUrl: versionResult.url,
276
- downloadApiUrl: downloadResult.url,
277
- sdkZipUrl: downloadResult.sdkZipUrl,
278
- zipPath,
279
- extractDir,
280
- resolvedSdkRoot,
281
- });
381
+ if (downloaded && downloadApiUrl && sdkZipUrl) {
382
+ writeIosSdkCacheMetadata({
383
+ metadataPath: cacheMetadataPath,
384
+ version,
385
+ date: versionResult.ios.date,
386
+ packageType,
387
+ plugins,
388
+ versionUrl: versionResult.url,
389
+ downloadApiUrl,
390
+ sdkZipUrl,
391
+ zipPath,
392
+ extractDir,
393
+ resolvedSdkRoot,
394
+ });
395
+ }
282
396
 
283
397
  return {
284
398
  version,
285
399
  versionDate: versionResult.ios.date,
286
400
  versionUrl: versionResult.url,
287
- downloadApiUrl: downloadResult.url,
288
- sdkZipUrl: downloadResult.sdkZipUrl,
401
+ downloadApiUrl,
402
+ sdkZipUrl,
289
403
  zipPath,
290
404
  extractDir,
291
405
  resolvedSdkRoot,
292
406
  iosConfigPath,
293
407
  cacheMetadataPath,
408
+ downloaded,
294
409
  };
295
410
  }
@@ -423,7 +423,8 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
423
423
  const { report, patch, binaryCopies } = await runPipeline(ctx, manifest(), { dryRun: true });
424
424
 
425
425
  expect(report.errors).toEqual([]);
426
- expect(report.warnings.join("\n")).toContain("would download GoogleService-Info.plist to native-sample/GoogleService-Info.plist");
426
+ expect(report.warnings.join("\n")).not.toContain("GoogleService-Info.plist");
427
+ expect((report.logs ?? []).join("\n")).toContain("would download GoogleService-Info.plist to native-sample/GoogleService-Info.plist");
427
428
  expect(plannedIosAsset(patch, binaryCopies, "TOPDataFirebasePlugin.framework")).toBe(true);
428
429
  } finally {
429
430
  fs.rmSync(tmp, { recursive: true, force: true });
@@ -2,16 +2,20 @@ import { afterEach, describe, expect, it, vi } from "vitest";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
5
6
  import {
6
7
  DEFAULT_IOS_SDK_PLUGINS,
7
8
  buildSdkHomeDownloadUrl,
8
9
  buildSdkHomeVersionUrl,
10
+ downloadIosSdkToBundled,
9
11
  fetchSdkHomeIosDownloadUrl,
10
12
  fetchSdkHomeIosVersion,
11
13
  fetchSdkHomeVersions,
14
+ resolveIosSdkDownloadPluginsFromRemoteConfig,
12
15
  resolveIosSdkRootFromDirectory,
13
16
  resolveIosSdkZipFileName,
14
17
  } from "../src/remote/sdkHomeDownload.js";
18
+ import { resolveIosSdkCacheLayout } from "../src/cache.js";
15
19
 
16
20
  describe("sdk-home iOS SDK download client", () => {
17
21
  afterEach(() => {
@@ -104,6 +108,56 @@ describe("sdk-home iOS SDK download client", () => {
104
108
  expect(resolveIosSdkZipFileName("1.6.0.5")).toBe("topSDK-ios--V1.6.0.5.zip");
105
109
  });
106
110
 
111
+ it("resolves SDK download plugins from the iOS remote config", () => {
112
+ expect(
113
+ resolveIosSdkDownloadPluginsFromRemoteConfig({
114
+ packageName: "com.example.app",
115
+ channel: "APPLE",
116
+ devicePlatform: "ios",
117
+ topsdk: { appId: "app", appSecret: "secret" },
118
+ sdkModules: {
119
+ login: {
120
+ guest: {},
121
+ email: {},
122
+ facebook: { clientId: "fb", scheme: "fb", secret: "token" },
123
+ google: { clientId: "google", scheme: "google" },
124
+ snapchat: { clientId: "snap", redirect: "snap://callback" },
125
+ line: { clientId: "line", scheme: "line" },
126
+ naver: { clientId: "naver", secret: "secret", scheme: "naver" },
127
+ kakao: { clientId: "kakao", scheme: "kakao" },
128
+ tiktok: { clientId: "tiktok", secret: "secret", redirect: "tiktok://callback" },
129
+ discord: { clientId: "discord", secret: "secret", redirect: "https://example.invalid/discord" },
130
+ apple: {},
131
+ },
132
+ payment: { appleIap: {} },
133
+ analytics: {
134
+ appsflyer: { devKey: "af", appleAppId: "123" },
135
+ firebase: { firebase_file_url: "https://cdn.example.invalid/GoogleService-Info.plist" },
136
+ adjust: { appCode: "adjust" },
137
+ facebookdata: { clientId: "fb", scheme: "fb", secret: "token" },
138
+ },
139
+ },
140
+ })
141
+ ).toEqual([
142
+ "guest",
143
+ "email",
144
+ "facebook",
145
+ "google",
146
+ "snapchat",
147
+ "line",
148
+ "naver",
149
+ "kakao",
150
+ "tiktok",
151
+ "discord",
152
+ "apple",
153
+ "apple_pay",
154
+ "dataAppsFlyer",
155
+ "dataFirebase",
156
+ "dataAdjust",
157
+ "dataFacebook",
158
+ ]);
159
+ });
160
+
107
161
  it("resolves top-level and nested extracted SDK layouts", () => {
108
162
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "meet-sdk-tool-ios-"));
109
163
  try {
@@ -121,4 +175,101 @@ describe("sdk-home iOS SDK download client", () => {
121
175
  fs.rmSync(root, { recursive: true, force: true });
122
176
  }
123
177
  });
178
+
179
+ it("reuses an existing iOS SDK cache for the latest server version", async () => {
180
+ const version = "99.88.77-cache-test";
181
+ const cacheRoot = fs.mkdtempSync(path.join(os.tmpdir(), "meet-sdk-tool-cache-"));
182
+ const cacheLayout = resolveIosSdkCacheLayout({
183
+ version,
184
+ packageType: "native",
185
+ plugins: [...DEFAULT_IOS_SDK_PLUGINS],
186
+ cacheRoot,
187
+ });
188
+ fs.mkdirSync(path.join(cacheLayout.extractDir, "sdk"), { recursive: true });
189
+ fs.mkdirSync(path.join(cacheLayout.extractDir, "plugins"), { recursive: true });
190
+ const fetchMock = vi.fn(async (input: string | URL) => {
191
+ const url = String(input);
192
+ if (url.includes("/sdk/home/sdk-download/getDownLoadUrl")) {
193
+ throw new Error("download URL should not be requested when cache is current");
194
+ }
195
+ return {
196
+ ok: true,
197
+ status: 200,
198
+ text: async () =>
199
+ JSON.stringify({
200
+ code: 200,
201
+ data: {
202
+ result: {
203
+ android: { ver: "1.0.0", date: "2026-01-01" },
204
+ ios: { ver: version, date: "2026-06-26" },
205
+ },
206
+ },
207
+ }),
208
+ };
209
+ });
210
+ vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
211
+
212
+ try {
213
+ const result = await downloadIosSdkToBundled(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."), {
214
+ baseUrl: "https://example.com",
215
+ cacheRoot,
216
+ });
217
+
218
+ expect(result.version).toBe(version);
219
+ expect(result.downloaded).toBe(false);
220
+ expect(result.resolvedSdkRoot).toBe(cacheLayout.extractDir);
221
+ expect(fetchMock).toHaveBeenCalledTimes(1);
222
+ } finally {
223
+ fs.rmSync(cacheRoot, { recursive: true, force: true });
224
+ }
225
+ });
226
+
227
+ it("reuses a higher local iOS SDK cache instead of downloading an older server version", async () => {
228
+ const serverVersion = "99.88.77-cache-test";
229
+ const cachedVersion = "99.88.78-cache-test";
230
+ const cacheRoot = fs.mkdtempSync(path.join(os.tmpdir(), "meet-sdk-tool-cache-"));
231
+ const cacheLayout = resolveIosSdkCacheLayout({
232
+ version: cachedVersion,
233
+ packageType: "native",
234
+ plugins: [...DEFAULT_IOS_SDK_PLUGINS],
235
+ cacheRoot,
236
+ });
237
+ fs.mkdirSync(path.join(cacheLayout.extractDir, "sdk"), { recursive: true });
238
+ fs.mkdirSync(path.join(cacheLayout.extractDir, "plugins"), { recursive: true });
239
+ const fetchMock = vi.fn(async (input: string | URL) => {
240
+ const url = String(input);
241
+ if (url.includes("/sdk/home/sdk-download/getDownLoadUrl")) {
242
+ throw new Error("download URL should not be requested when a higher cache is available");
243
+ }
244
+ return {
245
+ ok: true,
246
+ status: 200,
247
+ text: async () =>
248
+ JSON.stringify({
249
+ code: 200,
250
+ data: {
251
+ result: {
252
+ android: { ver: "1.0.0", date: "2026-01-01" },
253
+ ios: { ver: serverVersion, date: "2026-06-26" },
254
+ },
255
+ },
256
+ }),
257
+ };
258
+ });
259
+ vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
260
+
261
+ try {
262
+ const result = await downloadIosSdkToBundled(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."), {
263
+ baseUrl: "https://example.com",
264
+ cacheRoot,
265
+ });
266
+
267
+ expect(result.version).toBe(cachedVersion);
268
+ expect(result.downloaded).toBe(false);
269
+ expect(result.resolvedSdkRoot).toBe(cacheLayout.extractDir);
270
+ expect(fetchMock).toHaveBeenCalledTimes(1);
271
+ } finally {
272
+ fs.rmSync(cacheRoot, { recursive: true, force: true });
273
+ }
274
+ });
124
275
  });