From 25ece887b6d43680402e6f734818010424858c2d Mon Sep 17 00:00:00 2001 From: Mike Pitre <12040919+mikepitre@users.noreply.github.com> Date: Thu, 28 May 2026 15:17:31 -0400 Subject: [PATCH] fix(expo): reapply clerk-ios SPM dedupe --- .changeset/fresh-clocks-pay.md | 5 + .changeset/huge-moments-poke.md | 2 - packages/expo/app.plugin.js | 191 +++++++++++------- .../expo/src/__tests__/appPlugin.ios.test.js | 132 ++++++++++++ 4 files changed, 260 insertions(+), 70 deletions(-) create mode 100644 .changeset/fresh-clocks-pay.md delete mode 100644 .changeset/huge-moments-poke.md create mode 100644 packages/expo/src/__tests__/appPlugin.ios.test.js diff --git a/.changeset/fresh-clocks-pay.md b/.changeset/fresh-clocks-pay.md new file mode 100644 index 00000000000..e3142e80c90 --- /dev/null +++ b/.changeset/fresh-clocks-pay.md @@ -0,0 +1,5 @@ +--- +'@clerk/expo': patch +--- + +Update the iOS native SDK used by the Expo config plugin to clerk-ios 1.1.3 and make rerunning prebuild update the existing Swift package reference instead of adding duplicates. diff --git a/.changeset/huge-moments-poke.md b/.changeset/huge-moments-poke.md deleted file mode 100644 index a845151cc84..00000000000 --- a/.changeset/huge-moments-poke.md +++ /dev/null @@ -1,2 +0,0 @@ ---- ---- diff --git a/packages/expo/app.plugin.js b/packages/expo/app.plugin.js index c19c5d57a7b..c589d3d1526 100644 --- a/packages/expo/app.plugin.js +++ b/packages/expo/app.plugin.js @@ -20,10 +20,114 @@ const path = require('path'); const fs = require('fs'); const CLERK_IOS_REPO = 'https://github.com/clerk/clerk-ios.git'; -const CLERK_IOS_VERSION = '1.0.0'; +const CLERK_IOS_VERSION = '1.1.3'; +const CLERK_IOS_REQUIREMENT = { + kind: 'exactVersion', + version: CLERK_IOS_VERSION, +}; const CLERK_MIN_IOS_VERSION = '17.0'; +const normalizeRepositoryURL = repositoryURL => String(repositoryURL || '').replace(/^"|"$/g, ''); + +const isClerkIOSPackageReference = ref => normalizeRepositoryURL(ref?.repositoryURL) === CLERK_IOS_REPO; + +const removeObjectReference = (list, uuid) => { + if (!Array.isArray(list)) { + return; + } + + for (let index = list.length - 1; index >= 0; index--) { + if (list[index]?.value === uuid) { + list.splice(index, 1); + } + } +}; + +const removeSwiftPackageProductDependencies = (xcodeProject, packageUuids) => { + const productDependencies = xcodeProject.hash.project.objects.XCSwiftPackageProductDependency || {}; + const packageUuidSet = new Set(packageUuids); + + for (const [productUuid, dependency] of Object.entries(productDependencies)) { + if (!packageUuidSet.has(dependency?.package)) { + continue; + } + + delete productDependencies[productUuid]; + + const targets = xcodeProject.hash.project.objects.PBXNativeTarget || {}; + for (const target of Object.values(targets)) { + removeObjectReference(target?.packageProductDependencies, productUuid); + } + } +}; + +const removeSwiftPackageReferences = (xcodeProject, packageUuids) => { + const packageReferences = xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference || {}; + + for (const packageUuid of packageUuids) { + delete packageReferences[packageUuid]; + } + + const projects = xcodeProject.hash.project.objects.PBXProject || {}; + for (const project of Object.values(projects)) { + for (const packageUuid of packageUuids) { + removeObjectReference(project?.packageReferences, packageUuid); + } + } + + removeSwiftPackageProductDependencies(xcodeProject, packageUuids); +}; + +const findOrCreateClerkIOSPackageReference = xcodeProject => { + const packageReferences = xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference; + const existingEntries = Object.entries(packageReferences).filter(([, ref]) => isClerkIOSPackageReference(ref)); + + if (existingEntries.length > 0) { + const [uuid, ref] = existingEntries[0]; + ref.repositoryURL = CLERK_IOS_REPO; + ref.requirement = { ...CLERK_IOS_REQUIREMENT }; + + const duplicateUuids = existingEntries.slice(1).map(([duplicateUuid]) => duplicateUuid); + removeSwiftPackageReferences(xcodeProject, duplicateUuids); + + return uuid; + } + + const packageUuid = xcodeProject.generateUuid(); + packageReferences[packageUuid] = { + isa: 'XCRemoteSwiftPackageReference', + repositoryURL: CLERK_IOS_REPO, + requirement: { ...CLERK_IOS_REQUIREMENT }, + }; + return packageUuid; +}; + +const findOrCreateSwiftPackageProductDependency = (xcodeProject, packageUuid, productName) => { + const productDependencies = xcodeProject.hash.project.objects.XCSwiftPackageProductDependency; + const existingEntry = Object.entries(productDependencies).find( + ([, dep]) => dep?.package === packageUuid && dep?.productName === productName, + ); + + if (existingEntry) { + return existingEntry[0]; + } + + const productUuid = xcodeProject.generateUuid(); + productDependencies[productUuid] = { + isa: 'XCSwiftPackageProductDependency', + package: packageUuid, + productName, + }; + return productUuid; +}; + +const addObjectReference = (list, uuid, comment) => { + if (!list.some(ref => ref.value === uuid)) { + list.push({ value: uuid, comment }); + } +}; + const withClerkIOS = config => { console.log('✅ Clerk iOS plugin loaded'); @@ -97,42 +201,21 @@ const withClerkIOS = config => { const targetUuid = targets.uuid; const targetName = targets.name; - // Add Swift Package reference to the project - const packageUuid = xcodeProject.generateUuid(); const packageName = 'clerk-ios'; - // Add package reference to XCRemoteSwiftPackageReference section if (!xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference) { xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference = {}; } - xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference[packageUuid] = { - isa: 'XCRemoteSwiftPackageReference', - repositoryURL: CLERK_IOS_REPO, - requirement: { - kind: 'exactVersion', - version: CLERK_IOS_VERSION, - }, - }; + const packageUuid = findOrCreateClerkIOSPackageReference(xcodeProject); // Add package product dependencies (ClerkKit + ClerkKitUI) - const productUuidKit = xcodeProject.generateUuid(); - const productUuidKitUI = xcodeProject.generateUuid(); if (!xcodeProject.hash.project.objects.XCSwiftPackageProductDependency) { xcodeProject.hash.project.objects.XCSwiftPackageProductDependency = {}; } - xcodeProject.hash.project.objects.XCSwiftPackageProductDependency[productUuidKit] = { - isa: 'XCSwiftPackageProductDependency', - package: packageUuid, - productName: 'ClerkKit', - }; - - xcodeProject.hash.project.objects.XCSwiftPackageProductDependency[productUuidKitUI] = { - isa: 'XCSwiftPackageProductDependency', - package: packageUuid, - productName: 'ClerkKitUI', - }; + const productUuidKit = findOrCreateSwiftPackageProductDependency(xcodeProject, packageUuid, 'ClerkKit'); + const productUuidKitUI = findOrCreateSwiftPackageProductDependency(xcodeProject, packageUuid, 'ClerkKitUI'); // Add package to project's package references const projectSection = xcodeProject.hash.project.objects.PBXProject; @@ -143,18 +226,7 @@ const withClerkIOS = config => { project.packageReferences = []; } - // Check if package is already added - const alreadyAdded = project.packageReferences.some(ref => { - const refObj = xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference[ref.value]; - return refObj && refObj.repositoryURL === CLERK_IOS_REPO; - }); - - if (!alreadyAdded) { - project.packageReferences.push({ - value: packageUuid, - comment: packageName, - }); - } + addObjectReference(project.packageReferences, packageUuid, packageName); // Add package products to main app target const nativeTarget = xcodeProject.hash.project.objects.PBXNativeTarget[targetUuid]; @@ -162,21 +234,8 @@ const withClerkIOS = config => { nativeTarget.packageProductDependencies = []; } - const kitAlreadyAdded = nativeTarget.packageProductDependencies.some(dep => dep.value === productUuidKit); - if (!kitAlreadyAdded) { - nativeTarget.packageProductDependencies.push({ - value: productUuidKit, - comment: 'ClerkKit', - }); - } - - const kitUIAlreadyAdded = nativeTarget.packageProductDependencies.some(dep => dep.value === productUuidKitUI); - if (!kitUIAlreadyAdded) { - nativeTarget.packageProductDependencies.push({ - value: productUuidKitUI, - comment: 'ClerkKitUI', - }); - } + addObjectReference(nativeTarget.packageProductDependencies, productUuidKit, 'ClerkKit'); + addObjectReference(nativeTarget.packageProductDependencies, productUuidKitUI, 'ClerkKitUI'); // Also add packages to ClerkExpo pod target if it exists const allTargets = xcodeProject.hash.project.objects.PBXNativeTarget; @@ -186,21 +245,8 @@ const withClerkIOS = config => { target.packageProductDependencies = []; } - const podKitAdded = target.packageProductDependencies.some(dep => dep.value === productUuidKit); - if (!podKitAdded) { - target.packageProductDependencies.push({ - value: productUuidKit, - comment: 'ClerkKit', - }); - } - - const podKitUIAdded = target.packageProductDependencies.some(dep => dep.value === productUuidKitUI); - if (!podKitUIAdded) { - target.packageProductDependencies.push({ - value: productUuidKitUI, - comment: 'ClerkKitUI', - }); - } + addObjectReference(target.packageProductDependencies, productUuidKit, 'ClerkKit'); + addObjectReference(target.packageProductDependencies, productUuidKitUI, 'ClerkKitUI'); console.log(`✅ Added ClerkKit and ClerkKitUI packages to ClerkExpo pod target`); } @@ -716,4 +762,13 @@ const withClerkExpo = (config, props = {}) => { }; module.exports = withClerkExpo; -module.exports._testing = { validateThemeJson, isPlainObject, VALID_COLOR_KEYS, HEX_COLOR_REGEX }; +module.exports._testing = { + validateThemeJson, + isPlainObject, + VALID_COLOR_KEYS, + HEX_COLOR_REGEX, + CLERK_IOS_VERSION, + CLERK_IOS_REQUIREMENT, + findOrCreateClerkIOSPackageReference, + findOrCreateSwiftPackageProductDependency, +}; diff --git a/packages/expo/src/__tests__/appPlugin.ios.test.js b/packages/expo/src/__tests__/appPlugin.ios.test.js new file mode 100644 index 00000000000..cd3587b50ac --- /dev/null +++ b/packages/expo/src/__tests__/appPlugin.ios.test.js @@ -0,0 +1,132 @@ +import { describe, expect, test, vi } from 'vitest'; + +/* eslint-disable @typescript-eslint/no-require-imports -- CJS plugin, no ESM export */ +const { CLERK_IOS_REQUIREMENT, findOrCreateClerkIOSPackageReference, findOrCreateSwiftPackageProductDependency } = + require('../../app.plugin.js')._testing; +/* eslint-enable @typescript-eslint/no-require-imports */ + +const createXcodeProject = objects => ({ + generateUuid: vi.fn(), + hash: { + project: { + objects: { + XCRemoteSwiftPackageReference: {}, + XCSwiftPackageProductDependency: {}, + PBXProject: {}, + PBXNativeTarget: {}, + ...objects, + }, + }, + }, +}); + +describe('clerk-ios Swift package helpers', () => { + test('updates an existing clerk-ios package reference in place', () => { + const xcodeProject = createXcodeProject({ + XCRemoteSwiftPackageReference: { + PACKAGE_REF: { + isa: 'XCRemoteSwiftPackageReference', + repositoryURL: '"https://github.com/clerk/clerk-ios.git"', + requirement: { kind: 'exactVersion', version: '1.0.0' }, + }, + }, + }); + + const packageUuid = findOrCreateClerkIOSPackageReference(xcodeProject); + + expect(packageUuid).toBe('PACKAGE_REF'); + expect(xcodeProject.generateUuid).not.toHaveBeenCalled(); + expect(xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference.PACKAGE_REF).toMatchObject({ + repositoryURL: 'https://github.com/clerk/clerk-ios.git', + requirement: CLERK_IOS_REQUIREMENT, + }); + }); + + test('removes duplicate clerk-ios package references and their product dependencies', () => { + const xcodeProject = createXcodeProject({ + XCRemoteSwiftPackageReference: { + PACKAGE_REF: { + isa: 'XCRemoteSwiftPackageReference', + repositoryURL: 'https://github.com/clerk/clerk-ios.git', + requirement: { kind: 'exactVersion', version: '1.0.0' }, + }, + DUPLICATE_PACKAGE_REF: { + isa: 'XCRemoteSwiftPackageReference', + repositoryURL: 'https://github.com/clerk/clerk-ios.git', + requirement: CLERK_IOS_REQUIREMENT, + }, + }, + XCSwiftPackageProductDependency: { + KIT_REF: { + isa: 'XCSwiftPackageProductDependency', + package: 'PACKAGE_REF', + productName: 'ClerkKit', + }, + DUPLICATE_KIT_REF: { + isa: 'XCSwiftPackageProductDependency', + package: 'DUPLICATE_PACKAGE_REF', + productName: 'ClerkKit', + }, + }, + PBXProject: { + PROJECT_REF: { + packageReferences: [ + { value: 'PACKAGE_REF', comment: 'clerk-ios' }, + { value: 'DUPLICATE_PACKAGE_REF', comment: 'clerk-ios' }, + ], + }, + }, + PBXNativeTarget: { + TARGET_REF: { + packageProductDependencies: [ + { value: 'KIT_REF', comment: 'ClerkKit' }, + { value: 'DUPLICATE_KIT_REF', comment: 'ClerkKit' }, + ], + }, + }, + }); + + const packageUuid = findOrCreateClerkIOSPackageReference(xcodeProject); + + expect(packageUuid).toBe('PACKAGE_REF'); + expect(xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference).not.toHaveProperty('DUPLICATE_PACKAGE_REF'); + expect(xcodeProject.hash.project.objects.XCSwiftPackageProductDependency).not.toHaveProperty('DUPLICATE_KIT_REF'); + expect(xcodeProject.hash.project.objects.PBXProject.PROJECT_REF.packageReferences).toEqual([ + { value: 'PACKAGE_REF', comment: 'clerk-ios' }, + ]); + expect(xcodeProject.hash.project.objects.PBXNativeTarget.TARGET_REF.packageProductDependencies).toEqual([ + { value: 'KIT_REF', comment: 'ClerkKit' }, + ]); + }); + + test('reuses a matching Swift package product dependency', () => { + const xcodeProject = createXcodeProject({ + XCSwiftPackageProductDependency: { + KIT_REF: { + isa: 'XCSwiftPackageProductDependency', + package: 'PACKAGE_REF', + productName: 'ClerkKit', + }, + }, + }); + + const productUuid = findOrCreateSwiftPackageProductDependency(xcodeProject, 'PACKAGE_REF', 'ClerkKit'); + + expect(productUuid).toBe('KIT_REF'); + expect(xcodeProject.generateUuid).not.toHaveBeenCalled(); + }); + + test('creates a missing Swift package product dependency', () => { + const xcodeProject = createXcodeProject({}); + xcodeProject.generateUuid.mockReturnValueOnce('NEW_KIT_REF'); + + const productUuid = findOrCreateSwiftPackageProductDependency(xcodeProject, 'PACKAGE_REF', 'ClerkKit'); + + expect(productUuid).toBe('NEW_KIT_REF'); + expect(xcodeProject.hash.project.objects.XCSwiftPackageProductDependency.NEW_KIT_REF).toEqual({ + isa: 'XCSwiftPackageProductDependency', + package: 'PACKAGE_REF', + productName: 'ClerkKit', + }); + }); +});