@leonxin/meetgames 0.1.15 → 0.1.17

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.
@@ -12,6 +12,13 @@ const xcode = require("xcode") as {
12
12
  project: (pbxprojPath: string) => XcodeProject;
13
13
  };
14
14
 
15
+ const EXPECTED_SIGNATURE_BY_XCFRAMEWORK: Record<string, string> = {
16
+ "FBAEMKit.xcframework": '"AppleDeveloperProgram:V9WTTPBFK9:Meta Platforms, Inc."',
17
+ "FBSDKCoreKit.xcframework": '"AppleDeveloperProgram:V9WTTPBFK9:Meta Platforms, Inc."',
18
+ "FBSDKCoreKit_Basics.xcframework": '"AppleDeveloperProgram:V9WTTPBFK9:Meta Platforms, Inc."',
19
+ "FBSDKLoginKit.xcframework": '"AppleDeveloperProgram:V9WTTPBFK9:Meta Platforms, Inc."',
20
+ };
21
+
15
22
  export interface PbxContext {
16
23
  rel: string;
17
24
  proj: XcodeProject;
@@ -216,6 +223,36 @@ function frameworkFileType(file: string): string {
216
223
  return file.endsWith(".xcframework") ? "wrapper.xcframework" : "wrapper.framework";
217
224
  }
218
225
 
226
+ function expectedSignature(file: string): string | undefined {
227
+ return EXPECTED_SIGNATURE_BY_XCFRAMEWORK[path.basename(file)];
228
+ }
229
+
230
+ function formatLastKnownFileType(lastKnownFileType: string): string {
231
+ return lastKnownFileType === "wrapper.plug-in" ? '"wrapper.plug-in"' : quotePbxStringIfNeeded(lastKnownFileType);
232
+ }
233
+
234
+ function topSdkFileReference(file: string, lastKnownFileType: string): Record<string, unknown> {
235
+ const basename = path.basename(file);
236
+ const ref: Record<string, unknown> = {
237
+ isa: "PBXFileReference",
238
+ };
239
+ const signature = expectedSignature(file);
240
+ if (signature) ref.expectedSignature = signature;
241
+ ref.lastKnownFileType = formatLastKnownFileType(lastKnownFileType);
242
+ ref.name = basename;
243
+ ref.path = file;
244
+ ref.sourceTree = "SOURCE_ROOT";
245
+ return ref;
246
+ }
247
+
248
+ function normalizeTopSdkFileReference(ref: Record<string, unknown>, file: string, lastKnownFileType: string): void {
249
+ const normalized = topSdkFileReference(file, lastKnownFileType);
250
+ for (const [key, value] of Object.entries(normalized)) {
251
+ ref[key] = value;
252
+ }
253
+ delete ref.includeInIndex;
254
+ }
255
+
219
256
  function ensureFileRef(ctx: PbxContext, file: string, lastKnownFileType: string): string {
220
257
  const basename = path.basename(file);
221
258
  const existing = findFileRefUuid(ctx, file);
@@ -223,20 +260,13 @@ function ensureFileRef(ctx: PbxContext, file: string, lastKnownFileType: string)
223
260
  if (existing) {
224
261
  const ref = fileRefSection[existing];
225
262
  if (ref && typeof ref === "object") {
226
- (ref as Record<string, unknown>).lastKnownFileType = lastKnownFileType;
263
+ normalizeTopSdkFileReference(ref as Record<string, unknown>, file, lastKnownFileType);
227
264
  }
228
265
  return existing;
229
266
  }
230
267
 
231
268
  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
- };
269
+ fileRefSection[uuid] = topSdkFileReference(file, lastKnownFileType);
240
270
  fileRefSection[`${uuid}_comment`] = basename;
241
271
  return uuid;
242
272
  }
@@ -322,15 +352,14 @@ function addCopyFileManually(ctx: PbxContext, target: string, file: string): voi
322
352
  if (!fileRefUuid) {
323
353
  fileRefUuid = ctx.proj.generateUuid();
324
354
  const fileRefSection = objectSection(ctx.proj, "PBXFileReference");
325
- fileRefSection[fileRefUuid] = {
326
- isa: "PBXFileReference",
327
- name: `"${basename}"`,
328
- path: `"${file}"`,
329
- sourceTree: '"<group>"',
330
- lastKnownFileType: "wrapper.framework",
331
- includeInIndex: 0,
332
- };
355
+ fileRefSection[fileRefUuid] = topSdkFileReference(file, "wrapper.framework");
333
356
  fileRefSection[`${fileRefUuid}_comment`] = basename;
357
+ } else {
358
+ const fileRefSection = objectSection(ctx.proj, "PBXFileReference");
359
+ const ref = fileRefSection[fileRefUuid];
360
+ if (ref && typeof ref === "object") {
361
+ normalizeTopSdkFileReference(ref as Record<string, unknown>, file, "wrapper.framework");
362
+ }
334
363
  }
335
364
 
336
365
  const buildUuid = ctx.proj.generateUuid();
@@ -421,13 +450,7 @@ function addSourceFileManually(ctx: PbxContext, file: string, lastKnownFileType?
421
450
  const fileRefSection = objectSection(ctx.proj, "PBXFileReference");
422
451
  const buildFileSection = objectSection(ctx.proj, "PBXBuildFile");
423
452
 
424
- fileRefSection[fileRefUuid] = {
425
- isa: "PBXFileReference",
426
- name: `"${basename}"`,
427
- path: `"${file}"`,
428
- sourceTree: '"<group>"',
429
- lastKnownFileType: lastKnownFileType ?? sourceFileType(file),
430
- };
453
+ fileRefSection[fileRefUuid] = topSdkFileReference(file, lastKnownFileType ?? sourceFileType(file));
431
454
  fileRefSection[`${fileRefUuid}_comment`] = basename;
432
455
  buildFileSection[buildUuid] = {
433
456
  isa: "PBXBuildFile",
@@ -458,14 +481,7 @@ function addResourceFileManually(ctx: PbxContext, file: string, lastKnownFileTyp
458
481
  const fileRefSection = objectSection(ctx.proj, "PBXFileReference");
459
482
  const buildFileSection = objectSection(ctx.proj, "PBXBuildFile");
460
483
 
461
- fileRefSection[fileRefUuid] = {
462
- isa: "PBXFileReference",
463
- name: `"${basename}"`,
464
- path: `"${file}"`,
465
- sourceTree: '"<group>"',
466
- lastKnownFileType: resourceFileType(file, lastKnownFileType),
467
- includeInIndex: 0,
468
- };
484
+ fileRefSection[fileRefUuid] = topSdkFileReference(file, resourceFileType(file, lastKnownFileType));
469
485
  fileRefSection[`${fileRefUuid}_comment`] = basename;
470
486
  buildFileSection[buildUuid] = {
471
487
  isa: "PBXBuildFile",
@@ -33,9 +33,9 @@ export interface SdkHomeDownloadUrlRequest {
33
33
 
34
34
  export interface DownloadIosSdkOptions {
35
35
  baseUrl?: string;
36
- version?: string;
37
36
  plugins?: readonly string[] | string;
38
37
  packageType?: string;
38
+ cacheRoot?: string;
39
39
  signal?: AbortSignal;
40
40
  }
41
41
 
@@ -43,13 +43,14 @@ export interface DownloadIosSdkResult {
43
43
  version: string;
44
44
  versionDate: string;
45
45
  versionUrl: string;
46
- downloadApiUrl: string;
47
- sdkZipUrl: string;
46
+ downloadApiUrl?: string;
47
+ sdkZipUrl?: string;
48
48
  zipPath: string;
49
49
  extractDir: string;
50
50
  resolvedSdkRoot: string;
51
51
  iosConfigPath: string;
52
52
  cacheMetadataPath: string;
53
+ downloaded: boolean;
53
54
  }
54
55
 
55
56
  function isRecord(value: unknown): value is JsonRecord {
@@ -188,6 +189,68 @@ export function resolveIosSdkRootFromDirectory(bundledDir: string): string {
188
189
  throw new Error(`iOS SDK layout invalid under ${bundledDir}: need sdk/ + plugins/ at top level or under one version directory.`);
189
190
  }
190
191
 
192
+ function tryResolveIosSdkRootFromDirectory(bundledDir: string): string | null {
193
+ try {
194
+ return resolveIosSdkRootFromDirectory(bundledDir);
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ function compareSdkVersions(a: string, b: string): number {
201
+ const left = a.split(/[.-]/);
202
+ const right = b.split(/[.-]/);
203
+ const len = Math.max(left.length, right.length);
204
+ for (let i = 0; i < len; i += 1) {
205
+ const l = left[i] ?? "0";
206
+ const r = right[i] ?? "0";
207
+ const ln = /^\d+$/.test(l) ? Number(l) : null;
208
+ const rn = /^\d+$/.test(r) ? Number(r) : null;
209
+ if (ln !== null && rn !== null) {
210
+ if (ln !== rn) return ln - rn;
211
+ continue;
212
+ }
213
+ const cmp = l.localeCompare(r);
214
+ if (cmp !== 0) return cmp;
215
+ }
216
+ return 0;
217
+ }
218
+
219
+ function findReusableCachedIosSdk(params: {
220
+ cacheRoot?: string;
221
+ packageType: string;
222
+ plugins: readonly string[];
223
+ serverVersion: string;
224
+ }): { version: string; layout: ReturnType<typeof resolveIosSdkCacheLayout>; resolvedSdkRoot: string } | null {
225
+ const probe = resolveIosSdkCacheLayout({
226
+ version: "__probe__",
227
+ packageType: params.packageType,
228
+ plugins: params.plugins,
229
+ cacheRoot: params.cacheRoot,
230
+ });
231
+ const packageDir = path.dirname(path.dirname(probe.baseDir));
232
+ if (!fs.existsSync(packageDir)) return null;
233
+
234
+ let best: { version: string; layout: ReturnType<typeof resolveIosSdkCacheLayout>; resolvedSdkRoot: string } | null = null;
235
+ for (const ent of fs.readdirSync(packageDir, { withFileTypes: true })) {
236
+ if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
237
+ const comparedToServer = compareSdkVersions(ent.name, params.serverVersion);
238
+ if (comparedToServer < 0) continue;
239
+ const layout = resolveIosSdkCacheLayout({
240
+ version: ent.name,
241
+ packageType: params.packageType,
242
+ plugins: params.plugins,
243
+ cacheRoot: params.cacheRoot,
244
+ });
245
+ const resolvedSdkRoot = tryResolveIosSdkRootFromDirectory(layout.extractDir);
246
+ if (!resolvedSdkRoot) continue;
247
+ if (!best || compareSdkVersions(ent.name, best.version) > 0) {
248
+ best = { version: ent.name, layout, resolvedSdkRoot };
249
+ }
250
+ }
251
+ return best;
252
+ }
253
+
191
254
  export async function downloadBinaryFile(url: string, absPath: string, signal?: AbortSignal): Promise<void> {
192
255
  let res: Response;
193
256
  try {
@@ -236,25 +299,39 @@ export async function downloadIosSdkToBundled(
236
299
  const baseUrl = options.baseUrl || defaultSdkHomeApiBaseUrl;
237
300
  const packageType = options.packageType || DEFAULT_IOS_SDK_PACKAGE_TYPE;
238
301
  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,
302
+ const versionResult = await fetchSdkHomeIosVersion({ baseUrl, signal: options.signal });
303
+ const serverVersion = versionResult.ios.ver;
304
+ const reusableCache = findReusableCachedIosSdk({
305
+ cacheRoot: options.cacheRoot,
247
306
  packageType,
248
- signal: options.signal,
307
+ plugins,
308
+ serverVersion,
249
309
  });
250
- const cacheLayout = resolveIosSdkCacheLayout({ version, packageType, plugins });
310
+ const version = reusableCache?.version ?? serverVersion;
311
+ const cacheLayout =
312
+ reusableCache?.layout ?? resolveIosSdkCacheLayout({ version, packageType, plugins, cacheRoot: options.cacheRoot });
251
313
  const extractDir = cacheLayout.extractDir;
252
314
  const zipPath = cacheLayout.zipPath;
315
+ let downloadApiUrl: string | undefined;
316
+ let sdkZipUrl: string | undefined;
317
+ let downloaded = false;
253
318
 
254
319
  const downloadAndExtract = async (): Promise<void> => {
255
- await downloadBinaryFile(downloadResult.sdkZipUrl, zipPath, options.signal);
320
+ const cachedRoot = tryResolveIosSdkRootFromDirectory(extractDir);
321
+ if (cachedRoot) return;
322
+ const fetchedDownload = await fetchSdkHomeIosDownloadUrl({
323
+ baseUrl,
324
+ version,
325
+ plugins,
326
+ packageType,
327
+ signal: options.signal,
328
+ });
329
+ downloadApiUrl = fetchedDownload.url;
330
+ sdkZipUrl = fetchedDownload.sdkZipUrl;
331
+ await downloadBinaryFile(fetchedDownload.sdkZipUrl, zipPath, options.signal);
256
332
  fs.rmSync(extractDir, { recursive: true, force: true });
257
333
  await extractZip(zipPath, extractDir);
334
+ downloaded = true;
258
335
  };
259
336
  await withCacheLock(cacheLayout.lockDir, downloadAndExtract);
260
337
  const iosConfigPath = syncMeetSdkIosVersionToConfig({
@@ -266,30 +343,33 @@ export async function downloadIosSdkToBundled(
266
343
  });
267
344
  const resolvedSdkRoot = resolveIosSdkRootFromDirectory(extractDir);
268
345
  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
- });
346
+ if (downloaded && downloadApiUrl && sdkZipUrl) {
347
+ writeIosSdkCacheMetadata({
348
+ metadataPath: cacheMetadataPath,
349
+ version,
350
+ date: versionResult.ios.date,
351
+ packageType,
352
+ plugins,
353
+ versionUrl: versionResult.url,
354
+ downloadApiUrl,
355
+ sdkZipUrl,
356
+ zipPath,
357
+ extractDir,
358
+ resolvedSdkRoot,
359
+ });
360
+ }
282
361
 
283
362
  return {
284
363
  version,
285
364
  versionDate: versionResult.ios.date,
286
365
  versionUrl: versionResult.url,
287
- downloadApiUrl: downloadResult.url,
288
- sdkZipUrl: downloadResult.sdkZipUrl,
366
+ downloadApiUrl,
367
+ sdkZipUrl,
289
368
  zipPath,
290
369
  extractDir,
291
370
  resolvedSdkRoot,
292
371
  iosConfigPath,
293
372
  cacheMetadataPath,
373
+ downloaded,
294
374
  };
295
375
  }
@@ -159,6 +159,18 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
159
159
  expect(fs.existsSync(path.join(tmp, "topSDK", "TOPIAPPayPlugin.framework"))).toBe(true);
160
160
  expect(fs.readFileSync(path.join(tmp, "topSDK", "TopSDKInstall.swift"), "utf8")).toBe("");
161
161
  expect(fs.existsSync(path.join(tmp, "topSDK", "TopSDKSource.bundle"))).toBe(true);
162
+ expect(
163
+ fs.readlinkSync(
164
+ path.join(
165
+ tmp,
166
+ "topSDK",
167
+ "FBSDKLoginKit.xcframework",
168
+ "ios-arm64_x86_64-maccatalyst",
169
+ "FBSDKLoginKit.framework",
170
+ "FBSDKLoginKit"
171
+ )
172
+ )
173
+ ).toBe("Versions/Current/FBSDKLoginKit");
162
174
 
163
175
  const pbx = fs.readFileSync(path.join(tmp, "native-sample.xcodeproj", "project.pbxproj"), "utf8");
164
176
  expect(pbx).toContain("TOPCore.framework");
@@ -167,6 +179,13 @@ describe.skipIf(!hasIosProjectFixture)("pipeline ios fixture", () => {
167
179
  expect(pbx).toContain("TOPCoreModel.xcdatamodeld in Sources");
168
180
  expect(pbx).not.toContain("TOPCoreModel.xcdatamodeld in Resources");
169
181
  expect(pbx).not.toContain("FBSDKLoginKit.xcframework in Resources");
182
+ expect(pbx).toContain(
183
+ 'FBSDKLoginKit.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:V9WTTPBFK9:Meta Platforms, Inc."; lastKnownFileType = wrapper.xcframework; name = FBSDKLoginKit.xcframework; path = topSDK/FBSDKLoginKit.xcframework; sourceTree = SOURCE_ROOT; };'
184
+ );
185
+ expect(pbx).toContain("FBSDKCoreKit.xcframework");
186
+ expect(pbx).toContain("FBSDKCoreKit_Basics.xcframework");
187
+ expect(pbx).toContain("FBAEMKit.xcframework");
188
+ expect(pbx).not.toContain('path = "topSDK/FBSDKLoginKit.xcframework"; sourceTree = "<group>";');
170
189
  expect(pbx).toContain("LIBRARY_SEARCH_PATHS = \"$(SRCROOT)/topSDK\"");
171
190
  expect(pbx).toContain("SWIFT_VERSION = 5.0");
172
191
  expect(pbx).toContain('"-ObjC"');
@@ -2,16 +2,19 @@ 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,
12
14
  resolveIosSdkRootFromDirectory,
13
15
  resolveIosSdkZipFileName,
14
16
  } from "../src/remote/sdkHomeDownload.js";
17
+ import { resolveIosSdkCacheLayout } from "../src/cache.js";
15
18
 
16
19
  describe("sdk-home iOS SDK download client", () => {
17
20
  afterEach(() => {
@@ -121,4 +124,101 @@ describe("sdk-home iOS SDK download client", () => {
121
124
  fs.rmSync(root, { recursive: true, force: true });
122
125
  }
123
126
  });
127
+
128
+ it("reuses an existing iOS SDK cache for the latest server version", async () => {
129
+ const version = "99.88.77-cache-test";
130
+ const cacheRoot = fs.mkdtempSync(path.join(os.tmpdir(), "meet-sdk-tool-cache-"));
131
+ const cacheLayout = resolveIosSdkCacheLayout({
132
+ version,
133
+ packageType: "native",
134
+ plugins: [...DEFAULT_IOS_SDK_PLUGINS],
135
+ cacheRoot,
136
+ });
137
+ fs.mkdirSync(path.join(cacheLayout.extractDir, "sdk"), { recursive: true });
138
+ fs.mkdirSync(path.join(cacheLayout.extractDir, "plugins"), { recursive: true });
139
+ const fetchMock = vi.fn(async (input: string | URL) => {
140
+ const url = String(input);
141
+ if (url.includes("/sdk/home/sdk-download/getDownLoadUrl")) {
142
+ throw new Error("download URL should not be requested when cache is current");
143
+ }
144
+ return {
145
+ ok: true,
146
+ status: 200,
147
+ text: async () =>
148
+ JSON.stringify({
149
+ code: 200,
150
+ data: {
151
+ result: {
152
+ android: { ver: "1.0.0", date: "2026-01-01" },
153
+ ios: { ver: version, date: "2026-06-26" },
154
+ },
155
+ },
156
+ }),
157
+ };
158
+ });
159
+ vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
160
+
161
+ try {
162
+ const result = await downloadIosSdkToBundled(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."), {
163
+ baseUrl: "https://example.com",
164
+ cacheRoot,
165
+ });
166
+
167
+ expect(result.version).toBe(version);
168
+ expect(result.downloaded).toBe(false);
169
+ expect(result.resolvedSdkRoot).toBe(cacheLayout.extractDir);
170
+ expect(fetchMock).toHaveBeenCalledTimes(1);
171
+ } finally {
172
+ fs.rmSync(cacheRoot, { recursive: true, force: true });
173
+ }
174
+ });
175
+
176
+ it("reuses a higher local iOS SDK cache instead of downloading an older server version", async () => {
177
+ const serverVersion = "99.88.77-cache-test";
178
+ const cachedVersion = "99.88.78-cache-test";
179
+ const cacheRoot = fs.mkdtempSync(path.join(os.tmpdir(), "meet-sdk-tool-cache-"));
180
+ const cacheLayout = resolveIosSdkCacheLayout({
181
+ version: cachedVersion,
182
+ packageType: "native",
183
+ plugins: [...DEFAULT_IOS_SDK_PLUGINS],
184
+ cacheRoot,
185
+ });
186
+ fs.mkdirSync(path.join(cacheLayout.extractDir, "sdk"), { recursive: true });
187
+ fs.mkdirSync(path.join(cacheLayout.extractDir, "plugins"), { recursive: true });
188
+ const fetchMock = vi.fn(async (input: string | URL) => {
189
+ const url = String(input);
190
+ if (url.includes("/sdk/home/sdk-download/getDownLoadUrl")) {
191
+ throw new Error("download URL should not be requested when a higher cache is available");
192
+ }
193
+ return {
194
+ ok: true,
195
+ status: 200,
196
+ text: async () =>
197
+ JSON.stringify({
198
+ code: 200,
199
+ data: {
200
+ result: {
201
+ android: { ver: "1.0.0", date: "2026-01-01" },
202
+ ios: { ver: serverVersion, date: "2026-06-26" },
203
+ },
204
+ },
205
+ }),
206
+ };
207
+ });
208
+ vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
209
+
210
+ try {
211
+ const result = await downloadIosSdkToBundled(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."), {
212
+ baseUrl: "https://example.com",
213
+ cacheRoot,
214
+ });
215
+
216
+ expect(result.version).toBe(cachedVersion);
217
+ expect(result.downloaded).toBe(false);
218
+ expect(result.resolvedSdkRoot).toBe(cacheLayout.extractDir);
219
+ expect(fetchMock).toHaveBeenCalledTimes(1);
220
+ } finally {
221
+ fs.rmSync(cacheRoot, { recursive: true, force: true });
222
+ }
223
+ });
124
224
  });