@use-voltra/ios-client 2.1.1 → 2.2.0
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.
- package/README.md +61 -8
- package/build/commonjs/dynamic-widget/api.js +15 -0
- package/build/commonjs/dynamic-widget/api.js.map +1 -0
- package/build/commonjs/index.js +15 -7
- package/build/commonjs/index.js.map +1 -1
- package/build/commonjs/native/NativeVoltra.js.map +1 -1
- package/build/module/dynamic-widget/api.js +10 -0
- package/build/module/dynamic-widget/api.js.map +1 -0
- package/build/module/index.js +1 -0
- package/build/module/index.js.map +1 -1
- package/build/module/native/NativeVoltra.js.map +1 -1
- package/build/typescript/commonjs/dynamic-widget/api.d.ts +8 -0
- package/build/typescript/commonjs/dynamic-widget/api.d.ts.map +1 -0
- package/build/typescript/commonjs/index.d.ts +1 -0
- package/build/typescript/commonjs/index.d.ts.map +1 -1
- package/build/typescript/commonjs/native/NativeVoltra.d.ts +1 -0
- package/build/typescript/commonjs/native/NativeVoltra.d.ts.map +1 -1
- package/build/typescript/module/dynamic-widget/api.d.ts +8 -0
- package/build/typescript/module/dynamic-widget/api.d.ts.map +1 -0
- package/build/typescript/module/index.d.ts +1 -0
- package/build/typescript/module/index.d.ts.map +1 -1
- package/build/typescript/module/native/NativeVoltra.d.ts +1 -0
- package/build/typescript/module/native/NativeVoltra.d.ts.map +1 -1
- package/expo-plugin/build/cjs/index.js +3 -1
- package/expo-plugin/build/cjs/index.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/index.js +1 -1
- package/expo-plugin/build/cjs/ios-widget/index.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/podfile.js +38 -12
- package/expo-plugin/build/cjs/ios-widget/podfile.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/buildPhases.js +143 -107
- package/expo-plugin/build/cjs/ios-widget/xcode/buildPhases.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/configurationList.js +30 -22
- package/expo-plugin/build/cjs/ios-widget/xcode/configurationList.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/fileReferences.js +50 -0
- package/expo-plugin/build/cjs/ios-widget/xcode/fileReferences.js.map +1 -0
- package/expo-plugin/build/cjs/ios-widget/xcode/groups.js +45 -43
- package/expo-plugin/build/cjs/ios-widget/xcode/groups.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/index.js +77 -91
- package/expo-plugin/build/cjs/ios-widget/xcode/index.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/mainAppSettings.js +41 -39
- package/expo-plugin/build/cjs/ios-widget/xcode/mainAppSettings.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/productFile.js +2 -2
- package/expo-plugin/build/cjs/ios-widget/xcode/productFile.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/target.js +29 -63
- package/expo-plugin/build/cjs/ios-widget/xcode/target.js.map +1 -1
- package/expo-plugin/build/esm/index.js +3 -1
- package/expo-plugin/build/esm/index.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/index.js +1 -1
- package/expo-plugin/build/esm/ios-widget/index.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/podfile.js +36 -11
- package/expo-plugin/build/esm/ios-widget/podfile.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/buildPhases.js +143 -106
- package/expo-plugin/build/esm/ios-widget/xcode/buildPhases.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/configurationList.js +31 -22
- package/expo-plugin/build/esm/ios-widget/xcode/configurationList.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/fileReferences.js +45 -0
- package/expo-plugin/build/esm/ios-widget/xcode/fileReferences.js.map +1 -0
- package/expo-plugin/build/esm/ios-widget/xcode/groups.js +45 -42
- package/expo-plugin/build/esm/ios-widget/xcode/groups.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/index.js +81 -96
- package/expo-plugin/build/esm/ios-widget/xcode/index.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/mainAppSettings.js +41 -39
- package/expo-plugin/build/esm/ios-widget/xcode/mainAppSettings.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/productFile.js +3 -2
- package/expo-plugin/build/esm/ios-widget/xcode/productFile.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/target.js +28 -62
- package/expo-plugin/build/esm/ios-widget/xcode/target.js.map +1 -1
- package/expo-plugin/build/types/index.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/podfile.d.ts +19 -1
- package/expo-plugin/build/types/ios-widget/podfile.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/buildPhases.d.ts +3 -7
- package/expo-plugin/build/types/ios-widget/xcode/buildPhases.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/configurationList.d.ts +15 -10
- package/expo-plugin/build/types/ios-widget/xcode/configurationList.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/fileReferences.d.ts +16 -0
- package/expo-plugin/build/types/ios-widget/xcode/fileReferences.d.ts.map +1 -0
- package/expo-plugin/build/types/ios-widget/xcode/groups.d.ts +0 -4
- package/expo-plugin/build/types/ios-widget/xcode/groups.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/index.d.ts +22 -3
- package/expo-plugin/build/types/ios-widget/xcode/index.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/mainAppSettings.d.ts +16 -4
- package/expo-plugin/build/types/ios-widget/xcode/mainAppSettings.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/productFile.d.ts +0 -4
- package/expo-plugin/build/types/ios-widget/xcode/productFile.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/target.d.ts +11 -26
- package/expo-plugin/build/types/ios-widget/xcode/target.d.ts.map +1 -1
- package/expo-plugin/src/index.ts +5 -1
- package/expo-plugin/src/ios-widget/index.ts +1 -1
- package/expo-plugin/src/ios-widget/podfile.ts +46 -13
- package/expo-plugin/src/ios-widget/xcode/__fixtures__/fresh.pbxproj +197 -0
- package/expo-plugin/src/ios-widget/xcode/__fixtures__/with-existing-extension.pbxproj +321 -0
- package/expo-plugin/src/ios-widget/xcode/__fixtures__/with-per-config-signing.pbxproj +203 -0
- package/expo-plugin/src/ios-widget/xcode/__snapshots__/applyXcodeChanges.node.test.ts.snap +357 -0
- package/expo-plugin/src/ios-widget/xcode/__tests__/pbxConsistency.ts +130 -0
- package/expo-plugin/src/ios-widget/xcode/buildPhases.ts +151 -152
- package/expo-plugin/src/ios-widget/xcode/configurationList.ts +52 -35
- package/expo-plugin/src/ios-widget/xcode/fileReferences.ts +51 -0
- package/expo-plugin/src/ios-widget/xcode/groups.ts +52 -54
- package/expo-plugin/src/ios-widget/xcode/index.ts +107 -119
- package/expo-plugin/src/ios-widget/xcode/mainAppSettings.ts +59 -54
- package/expo-plugin/src/ios-widget/xcode/productFile.ts +3 -2
- package/expo-plugin/src/ios-widget/xcode/target.ts +31 -83
- package/ios/Package.swift +7 -1
- package/ios/app/NativeVoltra.mm +19 -0
- package/ios/app/VoltraModule.swift +18 -0
- package/ios/app/VoltraModuleImpl.swift +16 -0
- package/ios/shared/DynamicWidgetPropsCodec.swift +51 -0
- package/ios/shared/DynamicWidgetPropsStorage.swift +53 -0
- package/ios/shared/DynamicWidgetPropsStore.swift +50 -0
- package/ios/shared/DynamicWidgetRenderCoordinator.swift +31 -0
- package/ios/shared/DynamicWidgetUpdater.swift +30 -0
- package/ios/shared/VoltraConstants.swift +4 -0
- package/ios/shared/VoltraJSRenderer.swift +3 -1
- package/ios/shared/VoltraNode.swift +6 -2
- package/ios/shared/VoltraWidgetDefaults.swift +9 -1
- package/ios/target/VoltraClientWidgetRuntime.swift +17 -5
- package/ios/target/VoltraHomeWidget.swift +6 -2
- package/ios/target/VoltraWidget.swift +3 -1
- package/ios/ui/Helpers/VoltraDeepLinkResolver.swift +3 -1
- package/ios/ui/Helpers/VoltraProgressDriver.swift +6 -2
- package/ios/ui/Layout/VoltraFlexStackLayout.swift +9 -3
- package/ios/ui/Style/JSColorParser.swift +24 -8
- package/ios/ui/Style/JSGradientParser.swift +36 -12
- package/ios/ui/Style/JSStyleParser.swift +36 -12
- package/ios/ui/Style/LayoutStyle.swift +12 -4
- package/ios/ui/Views/VoltraChart.swift +52 -16
- package/ios/ui/Views/VoltraLinearGradient.swift +6 -2
- package/package.json +5 -5
- package/src/dynamic-widget/api.ts +25 -0
- package/src/index.ts +1 -0
- package/src/native/NativeVoltra.ts +1 -0
|
@@ -2,6 +2,7 @@ import { XcodeProject } from '@expo/config-plugins'
|
|
|
2
2
|
import * as util from 'util'
|
|
3
3
|
|
|
4
4
|
import type { IOSWidgetExtensionFiles } from '../../types'
|
|
5
|
+
import { ensureWidgetFileReference, normalizeRef } from './fileReferences'
|
|
5
6
|
|
|
6
7
|
const pbxFile = require('xcode/lib/pbxFile')
|
|
7
8
|
|
|
@@ -98,71 +99,77 @@ export function ensureWidgetBundleScriptPhase(xcodeProject: XcodeProject, target
|
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
export interface
|
|
102
|
+
export interface EnsureBuildPhasesOptions {
|
|
102
103
|
targetUuid: string
|
|
104
|
+
targetName: string
|
|
103
105
|
groupName: string
|
|
104
106
|
productFile: {
|
|
105
107
|
uuid: string
|
|
108
|
+
fileRef?: string
|
|
106
109
|
target?: string
|
|
107
110
|
basename: string
|
|
108
111
|
group: string
|
|
109
112
|
}
|
|
110
113
|
widgetFiles: IOSWidgetExtensionFiles
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface EnsureBuildPhasesOptions extends AddBuildPhasesOptions {
|
|
114
114
|
mainTargetUuid?: string
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
*
|
|
118
|
+
* Finds a `PBXCopyFilesBuildPhase` on the given target that embeds app extensions
|
|
119
|
+
* (`dstSubfolderSpec == 13`), regardless of its display name. Returns the phase object or null.
|
|
120
|
+
*
|
|
121
|
+
* A project that already contains another app extension (Share/Watch/expo-live-activity) will have
|
|
122
|
+
* such a phase — typically named "Embed App Extensions" — and the widget's product must be added to
|
|
123
|
+
* it rather than to a second, duplicate embed phase (issue #87).
|
|
119
124
|
*/
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const resourcePaths = [...assetDirectories, ...localizedStringResources]
|
|
127
|
-
|
|
128
|
-
// Sources build phase
|
|
129
|
-
xcodeProject.addBuildPhase(
|
|
130
|
-
[...swiftFiles, ...intentFiles],
|
|
131
|
-
'PBXSourcesBuildPhase',
|
|
132
|
-
'Sources',
|
|
133
|
-
targetUuid,
|
|
134
|
-
folderType,
|
|
135
|
-
buildPath
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
// Copy files build phase
|
|
139
|
-
xcodeProject.addBuildPhase(
|
|
140
|
-
[],
|
|
141
|
-
'PBXCopyFilesBuildPhase',
|
|
142
|
-
groupName,
|
|
143
|
-
xcodeProject.getFirstTarget().uuid,
|
|
144
|
-
folderType,
|
|
145
|
-
buildPath
|
|
146
|
-
)
|
|
125
|
+
function findExistingEmbedExtensionsPhase(xcodeProject: XcodeProject, targetUuid: string): any | null {
|
|
126
|
+
const nativeTargets = xcodeProject.pbxNativeTargetSection()
|
|
127
|
+
const target = nativeTargets[targetUuid]
|
|
128
|
+
if (!target?.buildPhases) {
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
147
131
|
|
|
148
|
-
xcodeProject.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
132
|
+
const copyFilesSection = xcodeProject.hash.project.objects['PBXCopyFilesBuildPhase'] || {}
|
|
133
|
+
for (const entry of target.buildPhases) {
|
|
134
|
+
const phase = copyFilesSection[normalizeRef(entry.value)]
|
|
135
|
+
if (phase && String(phase.dstSubfolderSpec) === '13') {
|
|
136
|
+
return phase
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
153
141
|
|
|
154
|
-
|
|
155
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Finds a target's build phase of the given pbx section type by walking the target's own
|
|
144
|
+
* `buildPhases` list and checking membership in the type's object section; the first match wins.
|
|
145
|
+
*
|
|
146
|
+
* This is deliberately semantic: pbxproj comments are decorative and other tools strip them, and
|
|
147
|
+
* the xcode lib's `buildPhaseObject` both relies on comments and silently falls back to a
|
|
148
|
+
* project-wide search that can return another target's phase (e.g. the main app's "Sources").
|
|
149
|
+
* The embed phase is the one exception with extra semantics — see
|
|
150
|
+
* {@link findExistingEmbedExtensionsPhase} (`dstSubfolderSpec == 13`).
|
|
151
|
+
*/
|
|
152
|
+
function findTargetPhaseByType(xcodeProject: XcodeProject, targetUuid: string, phaseType: string): any | null {
|
|
153
|
+
const target = xcodeProject.pbxNativeTargetSection()[targetUuid]
|
|
154
|
+
if (!target?.buildPhases) {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
const phaseSection = xcodeProject.hash.project.objects[phaseType] || {}
|
|
159
|
+
for (const entry of target.buildPhases) {
|
|
160
|
+
const phase = phaseSection[normalizeRef(entry.value)]
|
|
161
|
+
if (phase) {
|
|
162
|
+
return phase
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
/**
|
|
162
169
|
* Ensures all required build phases and files exist for the widget extension target.
|
|
163
170
|
*/
|
|
164
171
|
export function ensureBuildPhases(xcodeProject: XcodeProject, options: EnsureBuildPhasesOptions): void {
|
|
165
|
-
const { targetUuid, groupName, productFile, widgetFiles } = options
|
|
172
|
+
const { targetUuid, targetName, groupName, productFile, widgetFiles } = options
|
|
166
173
|
const buildPath = `""`
|
|
167
174
|
const folderType = 'app_extension'
|
|
168
175
|
const mainTargetUuid = options.mainTargetUuid ?? xcodeProject.getFirstTarget().uuid
|
|
@@ -170,60 +177,55 @@ export function ensureBuildPhases(xcodeProject: XcodeProject, options: EnsureBui
|
|
|
170
177
|
const { swiftFiles, intentFiles, assetDirectories, localizedStringResources } = widgetFiles
|
|
171
178
|
const resourcePaths = [...assetDirectories, ...localizedStringResources]
|
|
172
179
|
|
|
173
|
-
dedupeBuildPhasesForTarget(xcodeProject, targetUuid, 'PBXSourcesBuildPhase'
|
|
174
|
-
dedupeBuildPhasesForTarget(xcodeProject, targetUuid, 'PBXFrameworksBuildPhase'
|
|
175
|
-
|
|
180
|
+
dedupeBuildPhasesForTarget(xcodeProject, targetUuid, 'PBXSourcesBuildPhase')
|
|
181
|
+
dedupeBuildPhasesForTarget(xcodeProject, targetUuid, 'PBXFrameworksBuildPhase')
|
|
182
|
+
dedupeEmbedPhasesForTarget(xcodeProject, mainTargetUuid)
|
|
176
183
|
|
|
177
184
|
// Sources build phase
|
|
178
|
-
let sourcesPhase = xcodeProject
|
|
185
|
+
let sourcesPhase = findTargetPhaseByType(xcodeProject, targetUuid, 'PBXSourcesBuildPhase')
|
|
179
186
|
if (!sourcesPhase) {
|
|
180
|
-
xcodeProject.addBuildPhase(
|
|
181
|
-
|
|
182
|
-
'PBXSourcesBuildPhase',
|
|
183
|
-
'Sources',
|
|
184
|
-
targetUuid,
|
|
185
|
-
folderType,
|
|
186
|
-
buildPath
|
|
187
|
-
)
|
|
188
|
-
sourcesPhase = xcodeProject.buildPhaseObject('PBXSourcesBuildPhase', 'Sources', targetUuid)
|
|
187
|
+
xcodeProject.addBuildPhase([], 'PBXSourcesBuildPhase', 'Sources', targetUuid, folderType, buildPath)
|
|
188
|
+
sourcesPhase = findTargetPhaseByType(xcodeProject, targetUuid, 'PBXSourcesBuildPhase')
|
|
189
189
|
}
|
|
190
190
|
if (sourcesPhase) {
|
|
191
|
-
ensureBuildPhaseFiles(xcodeProject, sourcesPhase, [...swiftFiles, ...intentFiles])
|
|
191
|
+
ensureBuildPhaseFiles(xcodeProject, sourcesPhase, [...swiftFiles, ...intentFiles], targetName)
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
// Copy files build phase (embed extension into main app)
|
|
195
|
-
|
|
194
|
+
// Copy files build phase (embed extension into main app) — the embed phase is matched purely by
|
|
195
|
+
// its `dstSubfolderSpec == 13` semantics, both before and after creation
|
|
196
|
+
let copyFilesPhase = findExistingEmbedExtensionsPhase(xcodeProject, mainTargetUuid)
|
|
196
197
|
if (!copyFilesPhase) {
|
|
197
198
|
xcodeProject.addBuildPhase([], 'PBXCopyFilesBuildPhase', groupName, mainTargetUuid, folderType, buildPath)
|
|
198
|
-
copyFilesPhase = xcodeProject
|
|
199
|
+
copyFilesPhase = findExistingEmbedExtensionsPhase(xcodeProject, mainTargetUuid)
|
|
199
200
|
}
|
|
200
201
|
if (copyFilesPhase) {
|
|
201
202
|
ensureCopyFilesPhaseProduct(xcodeProject, copyFilesPhase, productFile)
|
|
202
203
|
}
|
|
203
204
|
|
|
204
205
|
// Frameworks build phase
|
|
205
|
-
const frameworksPhase = xcodeProject
|
|
206
|
+
const frameworksPhase = findTargetPhaseByType(xcodeProject, targetUuid, 'PBXFrameworksBuildPhase')
|
|
206
207
|
if (!frameworksPhase) {
|
|
207
208
|
xcodeProject.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', targetUuid, folderType, buildPath)
|
|
208
209
|
}
|
|
209
210
|
|
|
210
211
|
// Resources build phase
|
|
211
|
-
let resourcesPhase = xcodeProject
|
|
212
|
+
let resourcesPhase = findTargetPhaseByType(xcodeProject, targetUuid, 'PBXResourcesBuildPhase')
|
|
212
213
|
if (!resourcesPhase) {
|
|
213
|
-
xcodeProject.addBuildPhase([
|
|
214
|
-
resourcesPhase = xcodeProject
|
|
214
|
+
xcodeProject.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', targetUuid)
|
|
215
|
+
resourcesPhase = findTargetPhaseByType(xcodeProject, targetUuid, 'PBXResourcesBuildPhase')
|
|
215
216
|
}
|
|
216
217
|
if (resourcesPhase) {
|
|
217
|
-
ensureBuildPhaseFiles(xcodeProject, resourcesPhase,
|
|
218
|
+
ensureBuildPhaseFiles(xcodeProject, resourcesPhase, resourcePaths, targetName)
|
|
218
219
|
}
|
|
219
220
|
}
|
|
220
221
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Drops duplicate build phases of one pbx section type from a target, keeping the first phase in
|
|
224
|
+
* the target's `buildPhases` order. Membership in the type's object section is the only criterion —
|
|
225
|
+
* comments are decorative and other tools strip them. De-referenced phase objects are deleted so no
|
|
226
|
+
* orphans remain.
|
|
227
|
+
*/
|
|
228
|
+
function dedupeBuildPhasesForTarget(xcodeProject: XcodeProject, targetUuid: string, phaseType: string): void {
|
|
227
229
|
const nativeTargets = xcodeProject.pbxNativeTargetSection()
|
|
228
230
|
const target = nativeTargets[targetUuid]
|
|
229
231
|
if (!target?.buildPhases) {
|
|
@@ -231,114 +233,97 @@ function dedupeBuildPhasesForTarget(
|
|
|
231
233
|
}
|
|
232
234
|
|
|
233
235
|
const phaseSection = xcodeProject.hash.project.objects[phaseType] || {}
|
|
234
|
-
const matching = target.buildPhases.filter((entry: any) => phaseSection[entry.value])
|
|
236
|
+
const matching = target.buildPhases.filter((entry: any) => phaseSection[normalizeRef(entry.value)])
|
|
235
237
|
if (matching.length <= 1) {
|
|
236
238
|
return
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
const keep = matching
|
|
240
|
-
|
|
241
|
-
|
|
241
|
+
const keep = matching[0]
|
|
242
|
+
const removed = matching.filter((entry: any) => entry.value !== keep.value)
|
|
242
243
|
target.buildPhases = target.buildPhases.filter((entry: any) => {
|
|
243
|
-
if (!phaseSection[entry.value]) {
|
|
244
|
+
if (!phaseSection[normalizeRef(entry.value)]) {
|
|
244
245
|
return true
|
|
245
246
|
}
|
|
246
247
|
return entry.value === keep.value
|
|
247
248
|
})
|
|
249
|
+
deleteOrphanPhaseObjects(phaseSection, removed)
|
|
248
250
|
}
|
|
249
251
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Drops duplicate embed-app-extensions copy-files phases (`dstSubfolderSpec == 13`) from a target,
|
|
254
|
+
* keeping the first one in the target's `buildPhases` order. Other copy-files phases (different
|
|
255
|
+
* `dstSubfolderSpec`) are left untouched. De-referenced phase objects are deleted so no orphans
|
|
256
|
+
* remain.
|
|
257
|
+
*/
|
|
258
|
+
function dedupeEmbedPhasesForTarget(xcodeProject: XcodeProject, targetUuid: string): void {
|
|
256
259
|
const nativeTargets = xcodeProject.pbxNativeTargetSection()
|
|
257
260
|
const target = nativeTargets[targetUuid]
|
|
258
261
|
if (!target?.buildPhases) {
|
|
259
262
|
return
|
|
260
263
|
}
|
|
261
264
|
|
|
262
|
-
const phaseSection = xcodeProject.hash.project.objects[
|
|
263
|
-
const
|
|
265
|
+
const phaseSection = xcodeProject.hash.project.objects['PBXCopyFilesBuildPhase'] || {}
|
|
266
|
+
const isEmbedEntry = (entry: any) => {
|
|
267
|
+
const phase = phaseSection[normalizeRef(entry.value)]
|
|
268
|
+
return phase && String(phase.dstSubfolderSpec) === '13'
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const matching = target.buildPhases.filter(isEmbedEntry)
|
|
264
272
|
if (matching.length <= 1) {
|
|
265
273
|
return
|
|
266
274
|
}
|
|
267
275
|
|
|
268
276
|
const keep = matching[0]
|
|
277
|
+
const removed = matching.filter((entry: any) => entry.value !== keep.value)
|
|
269
278
|
target.buildPhases = target.buildPhases.filter((entry: any) => {
|
|
270
|
-
if (!
|
|
279
|
+
if (!isEmbedEntry(entry)) {
|
|
271
280
|
return true
|
|
272
281
|
}
|
|
273
282
|
return entry.value === keep.value
|
|
274
283
|
})
|
|
284
|
+
deleteOrphanPhaseObjects(phaseSection, removed)
|
|
275
285
|
}
|
|
276
286
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
for (const key of Object.keys(fileReferenceSection)) {
|
|
289
|
-
if (/_comment$/.test(key)) {
|
|
290
|
-
continue
|
|
291
|
-
}
|
|
292
|
-
const entry = fileReferenceSection[key]
|
|
293
|
-
const entryPath = normalizePath(entry?.path)
|
|
294
|
-
if (entryPath === normalizedPath) {
|
|
295
|
-
return key
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return null
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function findBuildFileKeyByFileRef(xcodeProject: XcodeProject, fileRef: string): string | null {
|
|
303
|
-
const buildFileSection = xcodeProject.pbxBuildFileSection()
|
|
304
|
-
for (const key of Object.keys(buildFileSection)) {
|
|
305
|
-
if (/_comment$/.test(key)) {
|
|
306
|
-
continue
|
|
307
|
-
}
|
|
308
|
-
const entry = buildFileSection[key]
|
|
309
|
-
if (entry?.fileRef === fileRef) {
|
|
310
|
-
return key
|
|
311
|
-
}
|
|
287
|
+
/**
|
|
288
|
+
* Deletes de-referenced build-phase objects (and their `_comment` keys) from a phase section, so
|
|
289
|
+
* dropping duplicate phase refs from a target does not leave orphan objects that fail the
|
|
290
|
+
* `[Xcodeproj]` consistency check.
|
|
291
|
+
*/
|
|
292
|
+
function deleteOrphanPhaseObjects(phaseSection: Record<string, any>, removedEntries: any[]): void {
|
|
293
|
+
for (const entry of removedEntries) {
|
|
294
|
+
const key = normalizeRef(entry.value)
|
|
295
|
+
delete phaseSection[key]
|
|
296
|
+
delete phaseSection[`${key}_comment`]
|
|
312
297
|
}
|
|
313
|
-
return null
|
|
314
298
|
}
|
|
315
299
|
|
|
316
|
-
|
|
317
|
-
|
|
300
|
+
/**
|
|
301
|
+
* Resolves (or creates) the PBXBuildFile for a widget file within a specific build phase.
|
|
302
|
+
*
|
|
303
|
+
* The lookup is scoped to the given phase's `files`: a PBXBuildFile is only reused when it is
|
|
304
|
+
* already part of THIS phase. This prevents adopting a build file that belongs to another target's
|
|
305
|
+
* phase — the same PBXFileReference (e.g. an asset catalog) may legitimately need its own build
|
|
306
|
+
* file per phase, and sharing one produces the "no parent for object" corruption (issue #87). When
|
|
307
|
+
* no match exists a new PBXBuildFile with a fresh UUID is created.
|
|
308
|
+
*/
|
|
309
|
+
function ensureBuildFile(xcodeProject: XcodeProject, filePath: string, buildPhase: any, targetName: string) {
|
|
310
|
+
const fileRef = ensureWidgetFileReference(xcodeProject, filePath, targetName)
|
|
318
311
|
const file = new pbxFile(filePath)
|
|
319
312
|
|
|
320
|
-
if (
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function ensureBuildFile(xcodeProject: XcodeProject, filePath: string) {
|
|
330
|
-
const fileReference = ensureFileReference(xcodeProject, filePath)
|
|
331
|
-
const existingBuildFile = findBuildFileKeyByFileRef(xcodeProject, fileReference.fileRef)
|
|
332
|
-
|
|
333
|
-
if (existingBuildFile) {
|
|
334
|
-
return { uuid: existingBuildFile, ...fileReference }
|
|
313
|
+
if (buildPhase?.files) {
|
|
314
|
+
const buildFileSection = xcodeProject.pbxBuildFileSection()
|
|
315
|
+
const existingInPhase = buildPhase.files.find((entry: any) => {
|
|
316
|
+
const buildFile = buildFileSection[normalizeRef(entry.value)]
|
|
317
|
+
return buildFile?.fileRef === fileRef
|
|
318
|
+
})
|
|
319
|
+
if (existingInPhase) {
|
|
320
|
+
return { uuid: normalizeRef(existingInPhase.value), fileRef, basename: file.basename, group: file.group }
|
|
321
|
+
}
|
|
335
322
|
}
|
|
336
323
|
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
file.
|
|
340
|
-
xcodeProject.addToPbxBuildFileSection(file)
|
|
341
|
-
return { uuid: file.uuid, ...fileReference, group: file.group }
|
|
324
|
+
const uuid = xcodeProject.generateUuid()
|
|
325
|
+
xcodeProject.addToPbxBuildFileSection({ uuid, fileRef, basename: file.basename, group: file.group } as any)
|
|
326
|
+
return { uuid, fileRef, basename: file.basename, group: file.group }
|
|
342
327
|
}
|
|
343
328
|
|
|
344
329
|
function buildPhaseHasFile(xcodeProject: XcodeProject, buildPhase: any, fileRef: string): boolean {
|
|
@@ -348,23 +333,28 @@ function buildPhaseHasFile(xcodeProject: XcodeProject, buildPhase: any, fileRef:
|
|
|
348
333
|
const buildFileSection = xcodeProject.pbxBuildFileSection()
|
|
349
334
|
|
|
350
335
|
return buildPhase.files.some((entry: any) => {
|
|
351
|
-
const buildFile = buildFileSection[entry.value]
|
|
336
|
+
const buildFile = buildFileSection[normalizeRef(entry.value)]
|
|
352
337
|
return buildFile?.fileRef === fileRef
|
|
353
338
|
})
|
|
354
339
|
}
|
|
355
340
|
|
|
356
|
-
function ensureBuildPhaseFiles(
|
|
341
|
+
function ensureBuildPhaseFiles(
|
|
342
|
+
xcodeProject: XcodeProject,
|
|
343
|
+
buildPhase: any,
|
|
344
|
+
filePaths: string[],
|
|
345
|
+
targetName: string
|
|
346
|
+
): void {
|
|
357
347
|
if (!buildPhase.files) {
|
|
358
348
|
buildPhase.files = []
|
|
359
349
|
}
|
|
360
350
|
|
|
361
351
|
for (const filePath of filePaths) {
|
|
362
|
-
const
|
|
363
|
-
if (buildPhaseHasFile(xcodeProject, buildPhase,
|
|
352
|
+
const fileRef = ensureWidgetFileReference(xcodeProject, filePath, targetName)
|
|
353
|
+
if (buildPhaseHasFile(xcodeProject, buildPhase, fileRef)) {
|
|
364
354
|
continue
|
|
365
355
|
}
|
|
366
356
|
|
|
367
|
-
const buildFile = ensureBuildFile(xcodeProject, filePath)
|
|
357
|
+
const buildFile = ensureBuildFile(xcodeProject, filePath, buildPhase, targetName)
|
|
368
358
|
buildPhase.files.push({
|
|
369
359
|
value: buildFile.uuid,
|
|
370
360
|
comment: util.format('%s in %s', buildFile.basename, buildFile.group),
|
|
@@ -377,18 +367,27 @@ function ensureCopyFilesPhaseProduct(xcodeProject: XcodeProject, buildPhase: any
|
|
|
377
367
|
buildPhase.files = []
|
|
378
368
|
}
|
|
379
369
|
|
|
380
|
-
const
|
|
381
|
-
|
|
370
|
+
const buildFileSection = xcodeProject.pbxBuildFileSection()
|
|
371
|
+
const alreadyInPhase = buildPhase.files.some((entry: any) => {
|
|
372
|
+
const buildFile = buildFileSection[normalizeRef(entry.value)]
|
|
373
|
+
return buildFile?.fileRef === productFile.fileRef
|
|
374
|
+
})
|
|
375
|
+
if (alreadyInPhase) {
|
|
382
376
|
return
|
|
383
377
|
}
|
|
384
378
|
|
|
385
|
-
|
|
386
|
-
|
|
379
|
+
// Reuse the product's build-file UUID only if it isn't already registered by another phase;
|
|
380
|
+
// otherwise mint a fresh UUID so the two phases never share one PBXBuildFile (issue #87).
|
|
381
|
+
let useUuid = productFile.uuid
|
|
382
|
+
if (buildFileSection[productFile.uuid]) {
|
|
383
|
+
useUuid = xcodeProject.generateUuid()
|
|
384
|
+
xcodeProject.addToPbxBuildFileSection({ ...productFile, uuid: useUuid })
|
|
385
|
+
} else {
|
|
387
386
|
xcodeProject.addToPbxBuildFileSection(productFile)
|
|
388
387
|
}
|
|
389
388
|
|
|
390
389
|
buildPhase.files.push({
|
|
391
|
-
value:
|
|
390
|
+
value: useUuid,
|
|
392
391
|
comment: util.format('%s in %s', productFile.basename, productFile.group),
|
|
393
392
|
})
|
|
394
393
|
}
|
|
@@ -1,34 +1,52 @@
|
|
|
1
1
|
import { XcodeProject } from '@expo/config-plugins'
|
|
2
2
|
|
|
3
3
|
import { IOS } from '../../constants'
|
|
4
|
+
import type { MainAppSigningSettings, MainAppTargetSettings } from './mainAppSettings'
|
|
4
5
|
|
|
5
|
-
export interface
|
|
6
|
+
export interface EnsureConfigurationListOptions {
|
|
6
7
|
targetName: string
|
|
7
8
|
bundleIdentifier: string
|
|
8
9
|
deploymentTarget: string
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* App marketing version (CFBundleShortVersionString). App Store Connect expects the appex
|
|
12
|
+
* version to match the app's, so this becomes the widget's MARKETING_VERSION.
|
|
13
|
+
*/
|
|
14
|
+
version?: string
|
|
15
|
+
/** App build number; becomes the widget's CURRENT_PROJECT_VERSION. */
|
|
16
|
+
buildNumber?: string
|
|
17
|
+
/** Main app signing settings, applied per configuration name (Debug→Debug, Release→Release). */
|
|
18
|
+
mainAppSettings?: MainAppTargetSettings | null
|
|
12
19
|
}
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Resolves the main app signing settings for a widget configuration name: an exact name match
|
|
23
|
+
* wins, otherwise the first available configuration's settings are used as fallback.
|
|
24
|
+
*/
|
|
25
|
+
function signingForConfiguration(
|
|
26
|
+
mainAppSettings: MainAppTargetSettings | null | undefined,
|
|
27
|
+
configurationName: string
|
|
28
|
+
): MainAppSigningSettings {
|
|
29
|
+
if (!mainAppSettings) {
|
|
30
|
+
return {}
|
|
31
|
+
}
|
|
32
|
+
return mainAppSettings.byConfigurationName[configurationName] ?? mainAppSettings.fallback
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createBuildSettings(options: EnsureConfigurationListOptions, configurationName: string) {
|
|
36
|
+
const { targetName, bundleIdentifier, deploymentTarget, version, buildNumber, mainAppSettings } = options
|
|
37
|
+
const { codeSignStyle, developmentTeam, provisioningProfileSpecifier } = signingForConfiguration(
|
|
38
|
+
mainAppSettings,
|
|
39
|
+
configurationName
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const buildSettings: Record<string, string> = {
|
|
25
43
|
PRODUCT_NAME: `"$(TARGET_NAME)"`,
|
|
26
44
|
SWIFT_VERSION: IOS.SWIFT_VERSION,
|
|
27
45
|
TARGETED_DEVICE_FAMILY: `"${IOS.DEVICE_FAMILY}"`,
|
|
28
46
|
INFOPLIST_FILE: `${targetName}/Info.plist`,
|
|
29
47
|
INFOPLIST_OUTPUT_FORMAT: `"xml"`,
|
|
30
|
-
CURRENT_PROJECT_VERSION: `"1"`,
|
|
31
|
-
MARKETING_VERSION: `"1.0"`,
|
|
48
|
+
CURRENT_PROJECT_VERSION: `"${buildNumber ?? '1'}"`,
|
|
49
|
+
MARKETING_VERSION: `"${version ?? '1.0'}"`,
|
|
32
50
|
IPHONEOS_DEPLOYMENT_TARGET: `"${deploymentTarget}"`,
|
|
33
51
|
PRODUCT_BUNDLE_IDENTIFIER: `"${bundleIdentifier}"`,
|
|
34
52
|
SWIFT_OPTIMIZATION_LEVEL: `"-Onone"`,
|
|
@@ -37,41 +55,37 @@ function createCommonBuildSettings(options: AddConfigurationListOptions) {
|
|
|
37
55
|
ASSETCATALOG_COMPILER_APPICON_NAME: '""',
|
|
38
56
|
}
|
|
39
57
|
|
|
40
|
-
// Synchronize code signing settings from main app target
|
|
58
|
+
// Synchronize code signing settings from the main app target's matching configuration
|
|
41
59
|
if (codeSignStyle) {
|
|
42
|
-
|
|
60
|
+
buildSettings.CODE_SIGN_STYLE = `"${codeSignStyle}"`
|
|
43
61
|
}
|
|
44
62
|
if (developmentTeam) {
|
|
45
|
-
|
|
63
|
+
buildSettings.DEVELOPMENT_TEAM = `"${developmentTeam}"`
|
|
46
64
|
}
|
|
47
65
|
if (provisioningProfileSpecifier) {
|
|
48
|
-
|
|
66
|
+
buildSettings.PROVISIONING_PROFILE_SPECIFIER = `"${provisioningProfileSpecifier}"`
|
|
49
67
|
}
|
|
50
68
|
|
|
51
|
-
return
|
|
69
|
+
return buildSettings
|
|
52
70
|
}
|
|
53
71
|
|
|
54
72
|
/**
|
|
55
|
-
* Adds
|
|
73
|
+
* Adds a fresh XCConfigurationList for the widget extension target. Internal create-only fallback
|
|
74
|
+
* for {@link ensureXCConfigurationList}; not part of the public pipeline.
|
|
56
75
|
*/
|
|
57
|
-
|
|
76
|
+
function addXCConfigurationList(xcodeProject: XcodeProject, options: EnsureConfigurationListOptions) {
|
|
58
77
|
const { targetName } = options
|
|
59
|
-
const commonBuildSettings = createCommonBuildSettings(options)
|
|
60
78
|
|
|
61
79
|
const buildConfigurationsList = [
|
|
62
80
|
{
|
|
63
81
|
name: 'Debug',
|
|
64
82
|
isa: 'XCBuildConfiguration',
|
|
65
|
-
buildSettings:
|
|
66
|
-
...commonBuildSettings,
|
|
67
|
-
},
|
|
83
|
+
buildSettings: createBuildSettings(options, 'Debug'),
|
|
68
84
|
},
|
|
69
85
|
{
|
|
70
86
|
name: 'Release',
|
|
71
87
|
isa: 'XCBuildConfiguration',
|
|
72
|
-
buildSettings:
|
|
73
|
-
...commonBuildSettings,
|
|
74
|
-
},
|
|
88
|
+
buildSettings: createBuildSettings(options, 'Release'),
|
|
75
89
|
},
|
|
76
90
|
]
|
|
77
91
|
|
|
@@ -85,11 +99,13 @@ export function addXCConfigurationList(xcodeProject: XcodeProject, options: AddC
|
|
|
85
99
|
}
|
|
86
100
|
|
|
87
101
|
/**
|
|
88
|
-
* Ensures an existing XCConfigurationList is updated, or adds a new one if missing.
|
|
102
|
+
* Ensures an existing XCConfigurationList is updated, or adds a new one if missing. Each widget
|
|
103
|
+
* configuration receives the build settings derived for its own name, so Debug and Release signing
|
|
104
|
+
* stay in sync with the main app's respective configurations.
|
|
89
105
|
*/
|
|
90
106
|
export function ensureXCConfigurationList(
|
|
91
107
|
xcodeProject: XcodeProject,
|
|
92
|
-
options:
|
|
108
|
+
options: EnsureConfigurationListOptions,
|
|
93
109
|
existingConfigurationListId?: string | { value?: string }
|
|
94
110
|
) {
|
|
95
111
|
const configurationListId =
|
|
@@ -103,7 +119,6 @@ export function ensureXCConfigurationList(
|
|
|
103
119
|
return addXCConfigurationList(xcodeProject, options)
|
|
104
120
|
}
|
|
105
121
|
|
|
106
|
-
const commonBuildSettings = createCommonBuildSettings(options)
|
|
107
122
|
const buildConfigurations = configurationList.buildConfigurations
|
|
108
123
|
const buildConfigurationSection = xcodeProject.pbxXCBuildConfigurationSection()
|
|
109
124
|
|
|
@@ -117,9 +132,11 @@ export function ensureXCConfigurationList(
|
|
|
117
132
|
continue
|
|
118
133
|
}
|
|
119
134
|
|
|
135
|
+
const configurationName =
|
|
136
|
+
typeof buildConfiguration.name === 'string' ? buildConfiguration.name.replace(/^"|"$/g, '') : ''
|
|
120
137
|
buildConfiguration.buildSettings = {
|
|
121
138
|
...(buildConfiguration.buildSettings ?? {}),
|
|
122
|
-
...
|
|
139
|
+
...createBuildSettings(options, configurationName),
|
|
123
140
|
}
|
|
124
141
|
}
|
|
125
142
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { XcodeProject } from '@expo/config-plugins'
|
|
2
|
+
|
|
3
|
+
const pbxFile = require('xcode/lib/pbxFile')
|
|
4
|
+
|
|
5
|
+
/** Strips surrounding quotes from a pbx path/name value. */
|
|
6
|
+
export function normalizePath(value: string | undefined): string {
|
|
7
|
+
if (!value) {
|
|
8
|
+
return ''
|
|
9
|
+
}
|
|
10
|
+
return value.replace(/^"|"$/g, '')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Strips a trailing `/* comment *\/` and whitespace, yielding the bare UUID of a pbx reference. */
|
|
14
|
+
export function normalizeRef(value: unknown): string {
|
|
15
|
+
return String(value)
|
|
16
|
+
.replace(/\/\*.*?\*\//g, '')
|
|
17
|
+
.trim()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolves the PBXFileReference UUID for a widget file, scoped to the widget's own PBXGroup.
|
|
22
|
+
*
|
|
23
|
+
* pbxproj paths are group-relative, so a bare path like `Assets.xcassets` collides with any other
|
|
24
|
+
* extension's identically named file. Scoping the lookup to the children of the widget group
|
|
25
|
+
* (identified by `targetName`) keeps the widget's references distinct from those, which is what
|
|
26
|
+
* prevents the shared-fileRef corruption behind issues #87/#32. When the widget group does not
|
|
27
|
+
* exist yet, a fresh reference is created.
|
|
28
|
+
*/
|
|
29
|
+
export function ensureWidgetFileReference(xcodeProject: XcodeProject, filePath: string, targetName: string): string {
|
|
30
|
+
const file = new pbxFile(filePath)
|
|
31
|
+
const fileReferenceSection = xcodeProject.pbxFileReferenceSection()
|
|
32
|
+
const group = xcodeProject.pbxGroupByName(targetName)
|
|
33
|
+
|
|
34
|
+
if (group?.children) {
|
|
35
|
+
for (const child of group.children) {
|
|
36
|
+
const refKey = normalizeRef(child.value)
|
|
37
|
+
const entry = fileReferenceSection[refKey]
|
|
38
|
+
if (!entry) {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
const entryPath = normalizePath(entry.path)
|
|
42
|
+
if (entryPath === file.path || entryPath === normalizePath(filePath)) {
|
|
43
|
+
return refKey
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
file.fileRef = xcodeProject.generateUuid()
|
|
49
|
+
xcodeProject.addToPbxFileReferenceSection(file)
|
|
50
|
+
return file.fileRef
|
|
51
|
+
}
|