Skip to content

Commit c24a965

Browse files
authored
fix(expo): pin clerk-ios and dedupe Expo SPM refs (#8689)
1 parent c3df67a commit c24a965

3 files changed

Lines changed: 260 additions & 68 deletions

File tree

.changeset/fresh-clocks-pay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/expo': patch
3+
---
4+
5+
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.

packages/expo/app.plugin.js

Lines changed: 123 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,114 @@ const path = require('path');
2020
const fs = require('fs');
2121

2222
const CLERK_IOS_REPO = 'https://github.com/clerk/clerk-ios.git';
23-
const CLERK_IOS_VERSION = '1.0.0';
23+
const CLERK_IOS_VERSION = '1.1.3';
24+
const CLERK_IOS_REQUIREMENT = {
25+
kind: 'exactVersion',
26+
version: CLERK_IOS_VERSION,
27+
};
2428

2529
const CLERK_MIN_IOS_VERSION = '17.0';
2630

31+
const normalizeRepositoryURL = repositoryURL => String(repositoryURL || '').replace(/^"|"$/g, '');
32+
33+
const isClerkIOSPackageReference = ref => normalizeRepositoryURL(ref?.repositoryURL) === CLERK_IOS_REPO;
34+
35+
const removeObjectReference = (list, uuid) => {
36+
if (!Array.isArray(list)) {
37+
return;
38+
}
39+
40+
for (let index = list.length - 1; index >= 0; index--) {
41+
if (list[index]?.value === uuid) {
42+
list.splice(index, 1);
43+
}
44+
}
45+
};
46+
47+
const removeSwiftPackageProductDependencies = (xcodeProject, packageUuids) => {
48+
const productDependencies = xcodeProject.hash.project.objects.XCSwiftPackageProductDependency || {};
49+
const packageUuidSet = new Set(packageUuids);
50+
51+
for (const [productUuid, dependency] of Object.entries(productDependencies)) {
52+
if (!packageUuidSet.has(dependency?.package)) {
53+
continue;
54+
}
55+
56+
delete productDependencies[productUuid];
57+
58+
const targets = xcodeProject.hash.project.objects.PBXNativeTarget || {};
59+
for (const target of Object.values(targets)) {
60+
removeObjectReference(target?.packageProductDependencies, productUuid);
61+
}
62+
}
63+
};
64+
65+
const removeSwiftPackageReferences = (xcodeProject, packageUuids) => {
66+
const packageReferences = xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference || {};
67+
68+
for (const packageUuid of packageUuids) {
69+
delete packageReferences[packageUuid];
70+
}
71+
72+
const projects = xcodeProject.hash.project.objects.PBXProject || {};
73+
for (const project of Object.values(projects)) {
74+
for (const packageUuid of packageUuids) {
75+
removeObjectReference(project?.packageReferences, packageUuid);
76+
}
77+
}
78+
79+
removeSwiftPackageProductDependencies(xcodeProject, packageUuids);
80+
};
81+
82+
const findOrCreateClerkIOSPackageReference = xcodeProject => {
83+
const packageReferences = xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference;
84+
const existingEntries = Object.entries(packageReferences).filter(([, ref]) => isClerkIOSPackageReference(ref));
85+
86+
if (existingEntries.length > 0) {
87+
const [uuid, ref] = existingEntries[0];
88+
ref.repositoryURL = CLERK_IOS_REPO;
89+
ref.requirement = { ...CLERK_IOS_REQUIREMENT };
90+
91+
const duplicateUuids = existingEntries.slice(1).map(([duplicateUuid]) => duplicateUuid);
92+
removeSwiftPackageReferences(xcodeProject, duplicateUuids);
93+
94+
return uuid;
95+
}
96+
97+
const packageUuid = xcodeProject.generateUuid();
98+
packageReferences[packageUuid] = {
99+
isa: 'XCRemoteSwiftPackageReference',
100+
repositoryURL: CLERK_IOS_REPO,
101+
requirement: { ...CLERK_IOS_REQUIREMENT },
102+
};
103+
return packageUuid;
104+
};
105+
106+
const findOrCreateSwiftPackageProductDependency = (xcodeProject, packageUuid, productName) => {
107+
const productDependencies = xcodeProject.hash.project.objects.XCSwiftPackageProductDependency;
108+
const existingEntry = Object.entries(productDependencies).find(
109+
([, dep]) => dep?.package === packageUuid && dep?.productName === productName,
110+
);
111+
112+
if (existingEntry) {
113+
return existingEntry[0];
114+
}
115+
116+
const productUuid = xcodeProject.generateUuid();
117+
productDependencies[productUuid] = {
118+
isa: 'XCSwiftPackageProductDependency',
119+
package: packageUuid,
120+
productName,
121+
};
122+
return productUuid;
123+
};
124+
125+
const addObjectReference = (list, uuid, comment) => {
126+
if (!list.some(ref => ref.value === uuid)) {
127+
list.push({ value: uuid, comment });
128+
}
129+
};
130+
27131
const withClerkIOS = config => {
28132
console.log('✅ Clerk iOS plugin loaded');
29133

@@ -97,42 +201,21 @@ const withClerkIOS = config => {
97201
const targetUuid = targets.uuid;
98202
const targetName = targets.name;
99203

100-
// Add Swift Package reference to the project
101-
const packageUuid = xcodeProject.generateUuid();
102204
const packageName = 'clerk-ios';
103205

104-
// Add package reference to XCRemoteSwiftPackageReference section
105206
if (!xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference) {
106207
xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference = {};
107208
}
108209

109-
xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference[packageUuid] = {
110-
isa: 'XCRemoteSwiftPackageReference',
111-
repositoryURL: CLERK_IOS_REPO,
112-
requirement: {
113-
kind: 'exactVersion',
114-
version: CLERK_IOS_VERSION,
115-
},
116-
};
210+
const packageUuid = findOrCreateClerkIOSPackageReference(xcodeProject);
117211

118212
// Add package product dependencies (ClerkKit + ClerkKitUI)
119-
const productUuidKit = xcodeProject.generateUuid();
120-
const productUuidKitUI = xcodeProject.generateUuid();
121213
if (!xcodeProject.hash.project.objects.XCSwiftPackageProductDependency) {
122214
xcodeProject.hash.project.objects.XCSwiftPackageProductDependency = {};
123215
}
124216

125-
xcodeProject.hash.project.objects.XCSwiftPackageProductDependency[productUuidKit] = {
126-
isa: 'XCSwiftPackageProductDependency',
127-
package: packageUuid,
128-
productName: 'ClerkKit',
129-
};
130-
131-
xcodeProject.hash.project.objects.XCSwiftPackageProductDependency[productUuidKitUI] = {
132-
isa: 'XCSwiftPackageProductDependency',
133-
package: packageUuid,
134-
productName: 'ClerkKitUI',
135-
};
217+
const productUuidKit = findOrCreateSwiftPackageProductDependency(xcodeProject, packageUuid, 'ClerkKit');
218+
const productUuidKitUI = findOrCreateSwiftPackageProductDependency(xcodeProject, packageUuid, 'ClerkKitUI');
136219

137220
// Add package to project's package references
138221
const projectSection = xcodeProject.hash.project.objects.PBXProject;
@@ -143,40 +226,16 @@ const withClerkIOS = config => {
143226
project.packageReferences = [];
144227
}
145228

146-
// Check if package is already added
147-
const alreadyAdded = project.packageReferences.some(ref => {
148-
const refObj = xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference[ref.value];
149-
return refObj && refObj.repositoryURL === CLERK_IOS_REPO;
150-
});
151-
152-
if (!alreadyAdded) {
153-
project.packageReferences.push({
154-
value: packageUuid,
155-
comment: packageName,
156-
});
157-
}
229+
addObjectReference(project.packageReferences, packageUuid, packageName);
158230

159231
// Add package products to main app target
160232
const nativeTarget = xcodeProject.hash.project.objects.PBXNativeTarget[targetUuid];
161233
if (!nativeTarget.packageProductDependencies) {
162234
nativeTarget.packageProductDependencies = [];
163235
}
164236

165-
const kitAlreadyAdded = nativeTarget.packageProductDependencies.some(dep => dep.value === productUuidKit);
166-
if (!kitAlreadyAdded) {
167-
nativeTarget.packageProductDependencies.push({
168-
value: productUuidKit,
169-
comment: 'ClerkKit',
170-
});
171-
}
172-
173-
const kitUIAlreadyAdded = nativeTarget.packageProductDependencies.some(dep => dep.value === productUuidKitUI);
174-
if (!kitUIAlreadyAdded) {
175-
nativeTarget.packageProductDependencies.push({
176-
value: productUuidKitUI,
177-
comment: 'ClerkKitUI',
178-
});
179-
}
237+
addObjectReference(nativeTarget.packageProductDependencies, productUuidKit, 'ClerkKit');
238+
addObjectReference(nativeTarget.packageProductDependencies, productUuidKitUI, 'ClerkKitUI');
180239

181240
// Also add packages to ClerkExpo pod target if it exists
182241
const allTargets = xcodeProject.hash.project.objects.PBXNativeTarget;
@@ -186,21 +245,8 @@ const withClerkIOS = config => {
186245
target.packageProductDependencies = [];
187246
}
188247

189-
const podKitAdded = target.packageProductDependencies.some(dep => dep.value === productUuidKit);
190-
if (!podKitAdded) {
191-
target.packageProductDependencies.push({
192-
value: productUuidKit,
193-
comment: 'ClerkKit',
194-
});
195-
}
196-
197-
const podKitUIAdded = target.packageProductDependencies.some(dep => dep.value === productUuidKitUI);
198-
if (!podKitUIAdded) {
199-
target.packageProductDependencies.push({
200-
value: productUuidKitUI,
201-
comment: 'ClerkKitUI',
202-
});
203-
}
248+
addObjectReference(target.packageProductDependencies, productUuidKit, 'ClerkKit');
249+
addObjectReference(target.packageProductDependencies, productUuidKitUI, 'ClerkKitUI');
204250

205251
console.log(`✅ Added ClerkKit and ClerkKitUI packages to ClerkExpo pod target`);
206252
}
@@ -716,4 +762,13 @@ const withClerkExpo = (config, props = {}) => {
716762
};
717763

718764
module.exports = withClerkExpo;
719-
module.exports._testing = { validateThemeJson, isPlainObject, VALID_COLOR_KEYS, HEX_COLOR_REGEX };
765+
module.exports._testing = {
766+
validateThemeJson,
767+
isPlainObject,
768+
VALID_COLOR_KEYS,
769+
HEX_COLOR_REGEX,
770+
CLERK_IOS_VERSION,
771+
CLERK_IOS_REQUIREMENT,
772+
findOrCreateClerkIOSPackageReference,
773+
findOrCreateSwiftPackageProductDependency,
774+
};
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { describe, expect, test, vi } from 'vitest';
2+
3+
/* eslint-disable @typescript-eslint/no-require-imports -- CJS plugin, no ESM export */
4+
const { CLERK_IOS_REQUIREMENT, findOrCreateClerkIOSPackageReference, findOrCreateSwiftPackageProductDependency } =
5+
require('../../app.plugin.js')._testing;
6+
/* eslint-enable @typescript-eslint/no-require-imports */
7+
8+
const createXcodeProject = objects => ({
9+
generateUuid: vi.fn(),
10+
hash: {
11+
project: {
12+
objects: {
13+
XCRemoteSwiftPackageReference: {},
14+
XCSwiftPackageProductDependency: {},
15+
PBXProject: {},
16+
PBXNativeTarget: {},
17+
...objects,
18+
},
19+
},
20+
},
21+
});
22+
23+
describe('clerk-ios Swift package helpers', () => {
24+
test('updates an existing clerk-ios package reference in place', () => {
25+
const xcodeProject = createXcodeProject({
26+
XCRemoteSwiftPackageReference: {
27+
PACKAGE_REF: {
28+
isa: 'XCRemoteSwiftPackageReference',
29+
repositoryURL: '"https://github.com/clerk/clerk-ios.git"',
30+
requirement: { kind: 'exactVersion', version: '1.0.0' },
31+
},
32+
},
33+
});
34+
35+
const packageUuid = findOrCreateClerkIOSPackageReference(xcodeProject);
36+
37+
expect(packageUuid).toBe('PACKAGE_REF');
38+
expect(xcodeProject.generateUuid).not.toHaveBeenCalled();
39+
expect(xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference.PACKAGE_REF).toMatchObject({
40+
repositoryURL: 'https://github.com/clerk/clerk-ios.git',
41+
requirement: CLERK_IOS_REQUIREMENT,
42+
});
43+
});
44+
45+
test('removes duplicate clerk-ios package references and their product dependencies', () => {
46+
const xcodeProject = createXcodeProject({
47+
XCRemoteSwiftPackageReference: {
48+
PACKAGE_REF: {
49+
isa: 'XCRemoteSwiftPackageReference',
50+
repositoryURL: 'https://github.com/clerk/clerk-ios.git',
51+
requirement: { kind: 'exactVersion', version: '1.0.0' },
52+
},
53+
DUPLICATE_PACKAGE_REF: {
54+
isa: 'XCRemoteSwiftPackageReference',
55+
repositoryURL: 'https://github.com/clerk/clerk-ios.git',
56+
requirement: CLERK_IOS_REQUIREMENT,
57+
},
58+
},
59+
XCSwiftPackageProductDependency: {
60+
KIT_REF: {
61+
isa: 'XCSwiftPackageProductDependency',
62+
package: 'PACKAGE_REF',
63+
productName: 'ClerkKit',
64+
},
65+
DUPLICATE_KIT_REF: {
66+
isa: 'XCSwiftPackageProductDependency',
67+
package: 'DUPLICATE_PACKAGE_REF',
68+
productName: 'ClerkKit',
69+
},
70+
},
71+
PBXProject: {
72+
PROJECT_REF: {
73+
packageReferences: [
74+
{ value: 'PACKAGE_REF', comment: 'clerk-ios' },
75+
{ value: 'DUPLICATE_PACKAGE_REF', comment: 'clerk-ios' },
76+
],
77+
},
78+
},
79+
PBXNativeTarget: {
80+
TARGET_REF: {
81+
packageProductDependencies: [
82+
{ value: 'KIT_REF', comment: 'ClerkKit' },
83+
{ value: 'DUPLICATE_KIT_REF', comment: 'ClerkKit' },
84+
],
85+
},
86+
},
87+
});
88+
89+
const packageUuid = findOrCreateClerkIOSPackageReference(xcodeProject);
90+
91+
expect(packageUuid).toBe('PACKAGE_REF');
92+
expect(xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference).not.toHaveProperty('DUPLICATE_PACKAGE_REF');
93+
expect(xcodeProject.hash.project.objects.XCSwiftPackageProductDependency).not.toHaveProperty('DUPLICATE_KIT_REF');
94+
expect(xcodeProject.hash.project.objects.PBXProject.PROJECT_REF.packageReferences).toEqual([
95+
{ value: 'PACKAGE_REF', comment: 'clerk-ios' },
96+
]);
97+
expect(xcodeProject.hash.project.objects.PBXNativeTarget.TARGET_REF.packageProductDependencies).toEqual([
98+
{ value: 'KIT_REF', comment: 'ClerkKit' },
99+
]);
100+
});
101+
102+
test('reuses a matching Swift package product dependency', () => {
103+
const xcodeProject = createXcodeProject({
104+
XCSwiftPackageProductDependency: {
105+
KIT_REF: {
106+
isa: 'XCSwiftPackageProductDependency',
107+
package: 'PACKAGE_REF',
108+
productName: 'ClerkKit',
109+
},
110+
},
111+
});
112+
113+
const productUuid = findOrCreateSwiftPackageProductDependency(xcodeProject, 'PACKAGE_REF', 'ClerkKit');
114+
115+
expect(productUuid).toBe('KIT_REF');
116+
expect(xcodeProject.generateUuid).not.toHaveBeenCalled();
117+
});
118+
119+
test('creates a missing Swift package product dependency', () => {
120+
const xcodeProject = createXcodeProject({});
121+
xcodeProject.generateUuid.mockReturnValueOnce('NEW_KIT_REF');
122+
123+
const productUuid = findOrCreateSwiftPackageProductDependency(xcodeProject, 'PACKAGE_REF', 'ClerkKit');
124+
125+
expect(productUuid).toBe('NEW_KIT_REF');
126+
expect(xcodeProject.hash.project.objects.XCSwiftPackageProductDependency.NEW_KIT_REF).toEqual({
127+
isa: 'XCSwiftPackageProductDependency',
128+
package: 'PACKAGE_REF',
129+
productName: 'ClerkKit',
130+
});
131+
});
132+
});

0 commit comments

Comments
 (0)