voltra 2.0.0 → 2.1.1
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/build/cjs/config/normalize.js +93 -0
- package/build/cjs/config/normalize.js.map +1 -1
- package/build/cjs/index.js +5 -2
- package/build/cjs/index.js.map +1 -1
- package/build/cjs/platforms/android/apply.js +11 -2
- package/build/cjs/platforms/android/apply.js.map +1 -1
- package/build/cjs/platforms/android/generated.js +152 -130
- package/build/cjs/platforms/android/generated.js.map +1 -1
- package/build/cjs/platforms/android/gradle.js +120 -0
- package/build/cjs/platforms/android/gradle.js.map +1 -0
- package/build/cjs/platforms/ios/generated.js +246 -142
- package/build/cjs/platforms/ios/generated.js.map +1 -1
- package/build/cjs/platforms/ios/xcodeTarget.js +68 -0
- package/build/cjs/platforms/ios/xcodeTarget.js.map +1 -1
- package/build/cjs/platforms/shared/widgetModule.js +151 -0
- package/build/cjs/platforms/shared/widgetModule.js.map +1 -0
- package/build/esm/config/normalize.js +93 -0
- package/build/esm/config/normalize.js.map +1 -1
- package/build/esm/index.js +1 -0
- package/build/esm/index.js.map +1 -1
- package/build/esm/platforms/android/apply.js +11 -2
- package/build/esm/platforms/android/apply.js.map +1 -1
- package/build/esm/platforms/android/generated.js +152 -97
- package/build/esm/platforms/android/generated.js.map +1 -1
- package/build/esm/platforms/android/gradle.js +112 -0
- package/build/esm/platforms/android/gradle.js.map +1 -0
- package/build/esm/platforms/ios/generated.js +246 -109
- package/build/esm/platforms/ios/generated.js.map +1 -1
- package/build/esm/platforms/ios/xcodeTarget.js +69 -1
- package/build/esm/platforms/ios/xcodeTarget.js.map +1 -1
- package/build/esm/platforms/shared/widgetModule.js +111 -0
- package/build/esm/platforms/shared/widgetModule.js.map +1 -0
- package/build/types/config/normalize.d.ts.map +1 -1
- package/build/types/config/types.d.ts +32 -0
- package/build/types/config/types.d.ts.map +1 -1
- package/build/types/index.d.ts +3 -1
- package/build/types/index.d.ts.map +1 -1
- package/build/types/platforms/android/apply.d.ts.map +1 -1
- package/build/types/platforms/android/generated.d.ts.map +1 -1
- package/build/types/platforms/android/gradle.d.ts +18 -0
- package/build/types/platforms/android/gradle.d.ts.map +1 -0
- package/build/types/platforms/ios/generated.d.ts.map +1 -1
- package/build/types/platforms/ios/xcodeTarget.d.ts.map +1 -1
- package/build/types/platforms/shared/widgetModule.d.ts +9 -0
- package/build/types/platforms/shared/widgetModule.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/apply/index.ts +309 -0
- package/src/apply/preflight.ts +172 -0
- package/src/bin.ts +14 -0
- package/src/commands/apply.ts +114 -0
- package/src/config/defaults.ts +34 -0
- package/src/config/load.ts +100 -0
- package/src/config/normalize.ts +562 -0
- package/src/config/types.ts +317 -0
- package/src/cosmiconfig.d.ts +21 -0
- package/src/dependencies/platformPackages.ts +75 -0
- package/src/discovery/android.ts +233 -0
- package/src/discovery/ios.ts +579 -0
- package/src/fs/path.ts +25 -0
- package/src/fs/readWrite.ts +102 -0
- package/src/git/status.ts +205 -0
- package/src/index.ts +204 -0
- package/src/platforms/android/apply.ts +106 -0
- package/src/platforms/android/generated.ts +1284 -0
- package/src/platforms/android/gradle.ts +155 -0
- package/src/platforms/android/manifest.ts +379 -0
- package/src/platforms/ios/apply.ts +139 -0
- package/src/platforms/ios/deploymentTarget.ts +47 -0
- package/src/platforms/ios/entitlements.ts +146 -0
- package/src/platforms/ios/generated.ts +1233 -0
- package/src/platforms/ios/mainAppEntitlements.ts +11 -0
- package/src/platforms/ios/plist.ts +321 -0
- package/src/platforms/ios/podfile.ts +255 -0
- package/src/platforms/ios/targetName.ts +11 -0
- package/src/platforms/ios/xcode.ts +181 -0
- package/src/platforms/ios/xcodeTarget.ts +899 -0
- package/src/platforms/shared/widgetModule.ts +153 -0
- package/src/reporting/clack.ts +67 -0
- package/src/reporting/summary.ts +116 -0
- package/src/state/diff.ts +21 -0
- package/src/state/files.ts +52 -0
- package/src/state/load.ts +66 -0
- package/src/state/save.ts +31 -0
- package/src/xml2js.d.ts +23 -0
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
PBXFileReference,
|
|
5
|
+
PBXFrameworksBuildPhase,
|
|
6
|
+
PBXGroup,
|
|
7
|
+
PBXNativeTarget,
|
|
8
|
+
PBXResourcesBuildPhase,
|
|
9
|
+
PBXShellScriptBuildPhase,
|
|
10
|
+
PBXSourcesBuildPhase,
|
|
11
|
+
XCBuildConfiguration,
|
|
12
|
+
XCConfigurationList,
|
|
13
|
+
} from '@bacons/xcode'
|
|
14
|
+
|
|
15
|
+
import { normalizeRelativePath, toRelativePath } from '../../fs/path'
|
|
16
|
+
import { pathExists } from '../../fs/readWrite'
|
|
17
|
+
import { VoltraCliError } from '../../reporting/summary'
|
|
18
|
+
import { resolveIOSWidgetTargetName } from './targetName'
|
|
19
|
+
import { ensureMainGroupChild, openIOSXcodeProject, saveIOSXcodeProject } from './xcode'
|
|
20
|
+
import { resolveMainAppEntitlementsPath } from './mainAppEntitlements'
|
|
21
|
+
import { needsEntitlementsMutation } from './entitlements'
|
|
22
|
+
import { VOLTRA_MIN_IOS_DEPLOYMENT_TARGET, maxIOSDeploymentTarget } from './deploymentTarget'
|
|
23
|
+
|
|
24
|
+
import type { IOSProjectDiscovery } from '../../discovery/ios'
|
|
25
|
+
import type { NormalizedVoltraIOSConfig } from '../../config/types'
|
|
26
|
+
import type { ReportedChange } from '../../reporting/summary'
|
|
27
|
+
import type { IOSXcodeProjectContext } from './xcode'
|
|
28
|
+
import type { BuildSettings } from '@bacons/xcode/build/json/types'
|
|
29
|
+
|
|
30
|
+
const IOS_APP_EXTENSION_PRODUCT_TYPE = 'com.apple.product-type.app-extension'
|
|
31
|
+
const PRODUCT_FILE_TYPE = 'wrapper.app-extension'
|
|
32
|
+
const SWIFT_FILE_TYPE = 'sourcecode.swift'
|
|
33
|
+
const STRINGS_FILE_TYPE = 'text.plist.strings'
|
|
34
|
+
const PLIST_FILE_TYPE = 'text.plist.xml'
|
|
35
|
+
const ASSET_CATALOG_FILE_TYPE = 'folder.assetcatalog'
|
|
36
|
+
const COPY_FILES_PHASE_NAME = 'Embed Foundation Extensions'
|
|
37
|
+
const DYNAMIC_WIDGET_BUNDLE_PHASE_NAME = 'Bundle Voltra Dynamic Widgets'
|
|
38
|
+
const SOURCE_EXTENSIONS = new Set(['.swift'])
|
|
39
|
+
const RESOURCE_EXTENSIONS = new Set(['.xcassets', '.strings', '.ttf', '.otf', '.woff', '.woff2'])
|
|
40
|
+
const DYNAMIC_WIDGET_BUNDLE_SHELL_SCRIPT = `if [[ "$CONFIGURATION" == *Debug* ]]; then
|
|
41
|
+
echo "Voltra: Debug build - Dynamic Widgets load from Metro, skipping bundling"
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if [[ -f "$SRCROOT/.xcode.env" ]]; then
|
|
46
|
+
source "$SRCROOT/.xcode.env"
|
|
47
|
+
fi
|
|
48
|
+
if [[ -f "$SRCROOT/.xcode.env.local" ]]; then
|
|
49
|
+
source "$SRCROOT/.xcode.env.local"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
export PROJECT_ROOT="\${PROJECT_ROOT:-$SRCROOT/..}"
|
|
53
|
+
NODE_BINARY="\${NODE_BINARY:-node}"
|
|
54
|
+
|
|
55
|
+
BUNDLER="$("$NODE_BINARY" - "$PROJECT_ROOT" <<'NODE'
|
|
56
|
+
const { createRequire } = require('node:module')
|
|
57
|
+
const path = require('node:path')
|
|
58
|
+
|
|
59
|
+
const projectRoot = process.argv[2]
|
|
60
|
+
const requireFromProject = createRequire(path.join(projectRoot, 'package.json'))
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const resolved = requireFromProject.resolve('@use-voltra/metro/bundle-widgets')
|
|
64
|
+
process.stdout.write(resolved)
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(
|
|
67
|
+
'error: Voltra widget bundler could not resolve @use-voltra/metro from ' +
|
|
68
|
+
projectRoot +
|
|
69
|
+
'. Install @use-voltra/metro in app project so release widgets can be baked.'
|
|
70
|
+
)
|
|
71
|
+
console.error(error && error.message ? error.message : String(error))
|
|
72
|
+
process.exit(1)
|
|
73
|
+
}
|
|
74
|
+
NODE
|
|
75
|
+
)"
|
|
76
|
+
if [[ -z "$BUNDLER" ]]; then
|
|
77
|
+
echo "Voltra: resolver returned empty string for @use-voltra/metro/bundle-widgets" >&2
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
echo "Voltra: widget bundler resolved to $BUNDLER"
|
|
82
|
+
"$NODE_BINARY" "$BUNDLER" --out-dir "$TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH" --platform ios --project-root "$PROJECT_ROOT"
|
|
83
|
+
`
|
|
84
|
+
|
|
85
|
+
export interface EnsureIOSWidgetTargetOptions {
|
|
86
|
+
projectRoot: string
|
|
87
|
+
ios: NormalizedVoltraIOSConfig
|
|
88
|
+
discovery: IOSProjectDiscovery
|
|
89
|
+
generatedFiles: string[]
|
|
90
|
+
previousGeneratedFiles?: string[]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface EnsureIOSWidgetTargetResult {
|
|
94
|
+
change?: ReportedChange
|
|
95
|
+
targetName: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class IOSWidgetTargetMutationError extends VoltraCliError {
|
|
99
|
+
constructor(message: string) {
|
|
100
|
+
super(message, 'VOLTRA_IOS_XCODE_TARGET_FAILED')
|
|
101
|
+
this.name = 'IOSWidgetTargetMutationError'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function ensureIOSWidgetTarget(
|
|
106
|
+
options: EnsureIOSWidgetTargetOptions
|
|
107
|
+
): Promise<EnsureIOSWidgetTargetResult> {
|
|
108
|
+
const { projectRoot, ios, discovery, generatedFiles, previousGeneratedFiles } = options
|
|
109
|
+
const targetName = resolveIOSWidgetTargetName(ios, discovery)
|
|
110
|
+
const context = openIOSXcodeProject(discovery)
|
|
111
|
+
const beforeSerialized = JSON.stringify(context.project.toJSON())
|
|
112
|
+
const productPath = `${targetName}.appex`
|
|
113
|
+
const nextGeneratedFiles = normalizeGeneratedFilePaths(generatedFiles, projectRoot, discovery)
|
|
114
|
+
const previousWidgetFiles = normalizeGeneratedFilePaths(previousGeneratedFiles ?? [], projectRoot, discovery)
|
|
115
|
+
const staleTargetNames = getStaleWidgetTargetNames(previousWidgetFiles, targetName)
|
|
116
|
+
const bundleIdentifier = resolveBundleIdentifier(context, discovery, targetName)
|
|
117
|
+
const codeSigning = getMainAppCodeSigningSettings(context)
|
|
118
|
+
const mainAppEntitlementsPath = await getMainAppEntitlementsBuildSetting(discovery, ios)
|
|
119
|
+
|
|
120
|
+
removeStaleWidgetTargets(context, staleTargetNames)
|
|
121
|
+
ensureMainAppEntitlementsBuildSetting(context, mainAppEntitlementsPath)
|
|
122
|
+
ensureMainAppDeploymentTarget(context)
|
|
123
|
+
ensureWidgetTarget(context, targetName, bundleIdentifier, ios.deploymentTarget, codeSigning)
|
|
124
|
+
|
|
125
|
+
const widgetTarget = getWidgetTarget(context, targetName)
|
|
126
|
+
const widgetGroup = ensureWidgetGroup(context, targetName)
|
|
127
|
+
const productFile = ensureProductFile(context, targetName, productPath)
|
|
128
|
+
|
|
129
|
+
sanitizeWidgetGroupChildren(widgetGroup)
|
|
130
|
+
|
|
131
|
+
widgetTarget.props.productReference = productFile
|
|
132
|
+
widgetTarget.props.productType = IOS_APP_EXTENSION_PRODUCT_TYPE
|
|
133
|
+
widgetTarget.props.productName = targetName
|
|
134
|
+
|
|
135
|
+
ensureTargetDependency(context, widgetTarget)
|
|
136
|
+
ensureTargetAttributes(context, widgetTarget)
|
|
137
|
+
ensureDynamicWidgetBundlePhase(
|
|
138
|
+
widgetTarget,
|
|
139
|
+
ios.widgets.some((widget) => widget.entry !== undefined)
|
|
140
|
+
)
|
|
141
|
+
removeStaleGeneratedFileReferences(context, widgetTarget, widgetGroup, previousWidgetFiles, nextGeneratedFiles)
|
|
142
|
+
ensureBuildPhases(context, widgetTarget, productFile, nextGeneratedFiles)
|
|
143
|
+
ensureWidgetGroupFiles(context, widgetGroup, targetName, nextGeneratedFiles)
|
|
144
|
+
removeEmptyWidgetGroups(context, staleTargetNames)
|
|
145
|
+
|
|
146
|
+
const changePath = toRelativePath(projectRoot, discovery.pbxprojPath)
|
|
147
|
+
const afterSerialized = JSON.stringify(context.project.toJSON())
|
|
148
|
+
|
|
149
|
+
if (beforeSerialized !== afterSerialized) {
|
|
150
|
+
await saveIOSXcodeProject(context)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
change: beforeSerialized === afterSerialized ? undefined : { kind: 'updated', path: changePath },
|
|
155
|
+
targetName,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function ensureWidgetTarget(
|
|
160
|
+
context: IOSXcodeProjectContext,
|
|
161
|
+
targetName: string,
|
|
162
|
+
bundleIdentifier: string,
|
|
163
|
+
deploymentTarget: string,
|
|
164
|
+
codeSigning: MainAppCodeSigningSettings
|
|
165
|
+
): PBXNativeTarget {
|
|
166
|
+
const existingTarget = getWidgetTargetOptional(context, targetName)
|
|
167
|
+
|
|
168
|
+
if (existingTarget) {
|
|
169
|
+
ensureBuildConfigurations(existingTarget, targetName, bundleIdentifier, deploymentTarget, codeSigning)
|
|
170
|
+
return existingTarget
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const buildConfigurationList = createBuildConfigurationList(
|
|
174
|
+
context,
|
|
175
|
+
targetName,
|
|
176
|
+
bundleIdentifier,
|
|
177
|
+
deploymentTarget,
|
|
178
|
+
codeSigning
|
|
179
|
+
)
|
|
180
|
+
const target = context.project.rootObject.createNativeTarget({
|
|
181
|
+
buildConfigurationList,
|
|
182
|
+
name: targetName,
|
|
183
|
+
productType: IOS_APP_EXTENSION_PRODUCT_TYPE,
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
target.props.productName = targetName
|
|
187
|
+
target.getSourcesBuildPhase()
|
|
188
|
+
target.getResourcesBuildPhase()
|
|
189
|
+
target.getFrameworksBuildPhase()
|
|
190
|
+
return target
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function createBuildConfigurationList(
|
|
194
|
+
context: IOSXcodeProjectContext,
|
|
195
|
+
targetName: string,
|
|
196
|
+
bundleIdentifier: string,
|
|
197
|
+
deploymentTarget: string,
|
|
198
|
+
codeSigning: MainAppCodeSigningSettings
|
|
199
|
+
): XCConfigurationList {
|
|
200
|
+
const configs = context.mainAppTarget.buildConfigurations.all.map((config) => {
|
|
201
|
+
return XCBuildConfiguration.create(context.project, {
|
|
202
|
+
name: config.props.name,
|
|
203
|
+
buildSettings: buildWidgetBuildSettings(
|
|
204
|
+
targetName,
|
|
205
|
+
bundleIdentifier,
|
|
206
|
+
deploymentTarget,
|
|
207
|
+
codeSigning,
|
|
208
|
+
config.props.name
|
|
209
|
+
),
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
return XCConfigurationList.create(context.project, {
|
|
214
|
+
buildConfigurations: configs,
|
|
215
|
+
defaultConfigurationName: context.mainAppTarget.buildConfigurations.default.props.name,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function ensureBuildConfigurations(
|
|
220
|
+
target: PBXNativeTarget,
|
|
221
|
+
targetName: string,
|
|
222
|
+
bundleIdentifier: string,
|
|
223
|
+
minimumDeploymentTarget: string,
|
|
224
|
+
codeSigning: MainAppCodeSigningSettings
|
|
225
|
+
): void {
|
|
226
|
+
const configurationList = target.props.buildConfigurationList
|
|
227
|
+
|
|
228
|
+
if (!configurationList) {
|
|
229
|
+
throw new IOSWidgetTargetMutationError(
|
|
230
|
+
`Widget target '${target.props.name}' is missing a build configuration list.`
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (const config of configurationList.props.buildConfigurations) {
|
|
235
|
+
const deploymentTarget = resolveBuildConfigurationDeploymentTarget(config, minimumDeploymentTarget)
|
|
236
|
+
|
|
237
|
+
Object.assign(
|
|
238
|
+
config.props.buildSettings,
|
|
239
|
+
buildWidgetBuildSettings(targetName, bundleIdentifier, deploymentTarget, codeSigning, config.props.name)
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function buildWidgetBuildSettings(
|
|
245
|
+
targetName: string,
|
|
246
|
+
bundleIdentifier: string,
|
|
247
|
+
deploymentTarget: string,
|
|
248
|
+
codeSigning: MainAppCodeSigningSettings,
|
|
249
|
+
configurationName: string
|
|
250
|
+
): BuildSettings & Record<string, string | undefined> {
|
|
251
|
+
const buildSettings: BuildSettings & Record<string, string | undefined> = {
|
|
252
|
+
ASSETCATALOG_COMPILER_APPICON_NAME: '',
|
|
253
|
+
CODE_SIGN_ENTITLEMENTS: `${targetName}/${targetName}.entitlements`,
|
|
254
|
+
CURRENT_PROJECT_VERSION: '1',
|
|
255
|
+
INFOPLIST_FILE: `${targetName}/Info.plist`,
|
|
256
|
+
INFOPLIST_OUTPUT_FORMAT: 'xml',
|
|
257
|
+
IPHONEOS_DEPLOYMENT_TARGET: deploymentTarget,
|
|
258
|
+
MARKETING_VERSION: '1.0',
|
|
259
|
+
OTHER_SWIFT_FLAGS: `$(inherited) -D EXPO_CONFIGURATION_${configurationName.toUpperCase()}`,
|
|
260
|
+
PRODUCT_BUNDLE_IDENTIFIER: bundleIdentifier,
|
|
261
|
+
PRODUCT_NAME: '$(TARGET_NAME)',
|
|
262
|
+
SWIFT_OPTIMIZATION_LEVEL: '-Onone',
|
|
263
|
+
SWIFT_VERSION: '5.0',
|
|
264
|
+
TARGETED_DEVICE_FAMILY: '1,2',
|
|
265
|
+
...(codeSigning.codeSignStyle ? { CODE_SIGN_STYLE: codeSigning.codeSignStyle } : {}),
|
|
266
|
+
...(codeSigning.developmentTeam ? { DEVELOPMENT_TEAM: codeSigning.developmentTeam } : {}),
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
buildSettings.APPLICATION_EXTENSION_API_ONLY = 'YES'
|
|
270
|
+
buildSettings.INFOPLIST_OUTPUT_FORMAT = 'xml'
|
|
271
|
+
|
|
272
|
+
if (codeSigning.provisioningProfileSpecifier) {
|
|
273
|
+
buildSettings.PROVISIONING_PROFILE_SPECIFIER = codeSigning.provisioningProfileSpecifier
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return buildSettings
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function getWidgetTarget(context: IOSXcodeProjectContext, targetName: string): PBXNativeTarget {
|
|
280
|
+
const target = getWidgetTargetOptional(context, targetName)
|
|
281
|
+
|
|
282
|
+
if (!target) {
|
|
283
|
+
throw new IOSWidgetTargetMutationError(
|
|
284
|
+
`Xcode project does not contain widget target '${targetName}' after mutation.`
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return target
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function getWidgetTargetOptional(context: IOSXcodeProjectContext, targetName: string): PBXNativeTarget | undefined {
|
|
292
|
+
return context.project.rootObject.props.targets.find((target): target is PBXNativeTarget => {
|
|
293
|
+
return (
|
|
294
|
+
PBXNativeTarget.is(target) &&
|
|
295
|
+
target.props.name === targetName &&
|
|
296
|
+
target.props.productType === IOS_APP_EXTENSION_PRODUCT_TYPE
|
|
297
|
+
)
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function ensureWidgetGroup(context: IOSXcodeProjectContext, targetName: string): PBXGroup {
|
|
302
|
+
const existingGroup = context.mainGroup.getChildGroups().find((group) => group.getDisplayName() === targetName)
|
|
303
|
+
if (existingGroup) {
|
|
304
|
+
existingGroup.props.path = targetName
|
|
305
|
+
return existingGroup
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return ensureMainGroupChild(context, targetName)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function ensureProductFile(context: IOSXcodeProjectContext, targetName: string, productPath: string): PBXFileReference {
|
|
312
|
+
const existingProduct = [...context.project.values()].find((object): object is PBXFileReference => {
|
|
313
|
+
return (
|
|
314
|
+
PBXFileReference.is(object) &&
|
|
315
|
+
stripQuotes(object.props.path) === productPath &&
|
|
316
|
+
object.props.sourceTree === 'BUILT_PRODUCTS_DIR'
|
|
317
|
+
)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
if (existingProduct) {
|
|
321
|
+
existingProduct.props.explicitFileType = PRODUCT_FILE_TYPE
|
|
322
|
+
existingProduct.props.path = productPath
|
|
323
|
+
existingProduct.props.sourceTree = 'BUILT_PRODUCTS_DIR'
|
|
324
|
+
return existingProduct
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return context.productsGroup.createNewProductRefForTarget(targetName, 'appExtension')
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function ensureTargetDependency(context: IOSXcodeProjectContext, widgetTarget: PBXNativeTarget): void {
|
|
331
|
+
context.mainAppTarget.target.addDependency(widgetTarget)
|
|
332
|
+
|
|
333
|
+
const copyFilesPhase = context.mainAppTarget.getCopyFilesBuildPhaseFor(widgetTarget)
|
|
334
|
+
copyFilesPhase.ensureDefaultsForTarget(widgetTarget)
|
|
335
|
+
copyFilesPhase.props.name = COPY_FILES_PHASE_NAME
|
|
336
|
+
|
|
337
|
+
const productReference = widgetTarget.props.productReference
|
|
338
|
+
if (!productReference) {
|
|
339
|
+
throw new IOSWidgetTargetMutationError(`Widget target '${widgetTarget.props.name}' is missing a product reference.`)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
copyFilesPhase.ensureFile({ fileRef: productReference })
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function ensureTargetAttributes(context: IOSXcodeProjectContext, widgetTarget: PBXNativeTarget): void {
|
|
346
|
+
const attributes = context.project.rootObject.props.attributes
|
|
347
|
+
const targetAttributes = (attributes.TargetAttributes ??= {}) as Record<string, { LastSwiftMigration?: string }>
|
|
348
|
+
targetAttributes[widgetTarget.uuid] ??= { LastSwiftMigration: '1250' }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function ensureDynamicWidgetBundlePhase(widgetTarget: PBXNativeTarget, enabled: boolean): void {
|
|
352
|
+
const matchingPhases = widgetTarget.props.buildPhases.filter(
|
|
353
|
+
(phase): phase is PBXShellScriptBuildPhase =>
|
|
354
|
+
PBXShellScriptBuildPhase.is(phase) && stripQuotes(phase.props.name) === DYNAMIC_WIDGET_BUNDLE_PHASE_NAME
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if (!enabled) {
|
|
358
|
+
for (const phase of matchingPhases) {
|
|
359
|
+
phase.removeFromProject()
|
|
360
|
+
}
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
const primaryPhase =
|
|
364
|
+
matchingPhases[0] ??
|
|
365
|
+
widgetTarget.createBuildPhase(PBXShellScriptBuildPhase, {
|
|
366
|
+
name: DYNAMIC_WIDGET_BUNDLE_PHASE_NAME,
|
|
367
|
+
shellPath: '/bin/sh',
|
|
368
|
+
shellScript: DYNAMIC_WIDGET_BUNDLE_SHELL_SCRIPT,
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
primaryPhase.props.name = DYNAMIC_WIDGET_BUNDLE_PHASE_NAME
|
|
372
|
+
primaryPhase.props.shellPath = '/bin/sh'
|
|
373
|
+
primaryPhase.props.shellScript = DYNAMIC_WIDGET_BUNDLE_SHELL_SCRIPT
|
|
374
|
+
primaryPhase.props.alwaysOutOfDate = 1
|
|
375
|
+
|
|
376
|
+
for (const duplicatePhase of matchingPhases.slice(1)) {
|
|
377
|
+
duplicatePhase.removeFromProject()
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function removeStaleWidgetTargets(context: IOSXcodeProjectContext, staleTargetNames: string[]): void {
|
|
382
|
+
for (const staleTargetName of staleTargetNames) {
|
|
383
|
+
const staleTarget = getWidgetTargetOptional(context, staleTargetName)
|
|
384
|
+
if (!staleTarget) {
|
|
385
|
+
continue
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
staleTarget.removeFromProject()
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function ensureBuildPhases(
|
|
393
|
+
context: IOSXcodeProjectContext,
|
|
394
|
+
widgetTarget: PBXNativeTarget,
|
|
395
|
+
productFile: PBXFileReference,
|
|
396
|
+
generatedFiles: string[]
|
|
397
|
+
): void {
|
|
398
|
+
const sources = widgetTarget.getSourcesBuildPhase()
|
|
399
|
+
const resources = widgetTarget.getResourcesBuildPhase()
|
|
400
|
+
widgetTarget.getFrameworksBuildPhase()
|
|
401
|
+
|
|
402
|
+
const fileReferences = getBuildPhaseFileReferences(context, generatedFiles)
|
|
403
|
+
|
|
404
|
+
for (const fileReference of fileReferences) {
|
|
405
|
+
const relativePath = getReferenceRelativePath(context, fileReference)
|
|
406
|
+
|
|
407
|
+
if (isSourceFile(relativePath)) {
|
|
408
|
+
sources.ensureFile({ fileRef: fileReference })
|
|
409
|
+
continue
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (isResourceFile(relativePath)) {
|
|
413
|
+
resources.ensureFile({ fileRef: fileReference })
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const copyFilesPhase = context.mainAppTarget.getCopyFilesBuildPhaseFor(widgetTarget)
|
|
418
|
+
copyFilesPhase.ensureDefaultsForTarget(widgetTarget)
|
|
419
|
+
copyFilesPhase.props.name = COPY_FILES_PHASE_NAME
|
|
420
|
+
copyFilesPhase.ensureFile({ fileRef: productFile })
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function getBuildPhaseFileReferences(context: IOSXcodeProjectContext, generatedFiles: string[]): PBXFileReference[] {
|
|
424
|
+
const references = new Map<string, PBXFileReference>()
|
|
425
|
+
|
|
426
|
+
for (const file of generatedFiles) {
|
|
427
|
+
const buildPhasePath = getBuildPhaseReferencePath(file)
|
|
428
|
+
if (!buildPhasePath || references.has(buildPhasePath)) {
|
|
429
|
+
continue
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
references.set(buildPhasePath, ensureGeneratedFileReference(context, buildPhasePath))
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return [...references.values()]
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function removeStaleGeneratedFileReferences(
|
|
439
|
+
context: IOSXcodeProjectContext,
|
|
440
|
+
widgetTarget: PBXNativeTarget,
|
|
441
|
+
widgetGroup: PBXGroup,
|
|
442
|
+
previousGeneratedFiles: string[],
|
|
443
|
+
generatedFiles: string[]
|
|
444
|
+
): void {
|
|
445
|
+
const staleReferencePaths = getStaleReferencePaths(previousGeneratedFiles, generatedFiles)
|
|
446
|
+
|
|
447
|
+
if (staleReferencePaths.size === 0) {
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const staleReferences = [...context.project.values()].filter((object): object is PBXFileReference => {
|
|
452
|
+
if (!PBXFileReference.is(object)) {
|
|
453
|
+
return false
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const relativePath = getReferenceRelativePath(context, object)
|
|
457
|
+
return staleReferencePaths.has(relativePath)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
for (const reference of staleReferences) {
|
|
461
|
+
removeFileReferenceFromTargetBuildPhases(widgetTarget, reference)
|
|
462
|
+
removeFileReferenceFromGroupTree(widgetGroup, reference)
|
|
463
|
+
reference.removeFromProject()
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function ensureWidgetGroupFiles(
|
|
468
|
+
context: IOSXcodeProjectContext,
|
|
469
|
+
widgetGroup: PBXGroup,
|
|
470
|
+
targetName: string,
|
|
471
|
+
generatedFiles: string[]
|
|
472
|
+
): void {
|
|
473
|
+
const groupedReferencePaths = new Set<string>()
|
|
474
|
+
|
|
475
|
+
for (const file of generatedFiles) {
|
|
476
|
+
const groupReferencePath = getGroupReferencePath(file)
|
|
477
|
+
if (!groupReferencePath || groupedReferencePaths.has(groupReferencePath)) {
|
|
478
|
+
continue
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
groupedReferencePaths.add(groupReferencePath)
|
|
482
|
+
const reference = ensureGeneratedFileReference(context, groupReferencePath)
|
|
483
|
+
const relativeToTarget = getPathRelativeToTarget(groupReferencePath, targetName)
|
|
484
|
+
|
|
485
|
+
if (!relativeToTarget) {
|
|
486
|
+
continue
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (relativeToTarget.includes('/')) {
|
|
490
|
+
const parentGroup = ensureParentGroup(widgetGroup, relativeToTarget)
|
|
491
|
+
removeGroupReference(widgetGroup, reference)
|
|
492
|
+
ensureGroupContainsReference(parentGroup, reference)
|
|
493
|
+
continue
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
ensureGroupContainsReference(widgetGroup, reference)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function removeEmptyWidgetGroups(context: IOSXcodeProjectContext, staleTargetNames: string[]): void {
|
|
501
|
+
for (const staleTargetName of staleTargetNames) {
|
|
502
|
+
const staleGroup = context.mainGroup.getChildGroups().find((group) => group.getDisplayName() === staleTargetName)
|
|
503
|
+
|
|
504
|
+
if (!staleGroup || staleGroup.props.children.length > 0) {
|
|
505
|
+
continue
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
staleGroup.removeFromProject()
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function removeFileReferenceFromTargetBuildPhases(target: PBXNativeTarget, reference: PBXFileReference): void {
|
|
513
|
+
for (const phase of [
|
|
514
|
+
target.getSourcesBuildPhase(),
|
|
515
|
+
target.getResourcesBuildPhase(),
|
|
516
|
+
target.getFrameworksBuildPhase(),
|
|
517
|
+
]) {
|
|
518
|
+
removeBuildPhaseReference(phase, reference)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function removeBuildPhaseReference(
|
|
523
|
+
phase: PBXSourcesBuildPhase | PBXResourcesBuildPhase | PBXFrameworksBuildPhase,
|
|
524
|
+
reference: PBXFileReference
|
|
525
|
+
): void {
|
|
526
|
+
if (phase.includesFile(reference)) {
|
|
527
|
+
phase.removeFileReference(reference)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function ensureGeneratedFileReference(context: IOSXcodeProjectContext, relativeFilePath: string): PBXFileReference {
|
|
532
|
+
const absolutePath = path.join(context.project.getProjectRoot(), relativeFilePath)
|
|
533
|
+
const existingReference = context.project.getReferenceForPath(absolutePath)
|
|
534
|
+
|
|
535
|
+
if (existingReference) {
|
|
536
|
+
applyFileType(existingReference, relativeFilePath)
|
|
537
|
+
return existingReference
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const targetName = path.dirname(relativeFilePath).split(path.sep)[0]
|
|
541
|
+
const widgetGroup = ensureWidgetGroup(context, targetName)
|
|
542
|
+
const pathWithinGroup = getPathRelativeToTarget(relativeFilePath, targetName)
|
|
543
|
+
|
|
544
|
+
if (!pathWithinGroup) {
|
|
545
|
+
throw new IOSWidgetTargetMutationError(`Generated iOS file is outside widget target directory: ${relativeFilePath}`)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const parentGroup = ensureParentGroup(widgetGroup, pathWithinGroup)
|
|
549
|
+
const fileReference = parentGroup.createFile({ path: path.basename(pathWithinGroup) })
|
|
550
|
+
applyFileType(fileReference, relativeFilePath)
|
|
551
|
+
return fileReference
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function ensureParentGroup(rootGroup: PBXGroup, relativePath: string): PBXGroup {
|
|
555
|
+
const directories = path.dirname(relativePath)
|
|
556
|
+
if (directories === '.' || directories === '') {
|
|
557
|
+
return rootGroup
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const group = rootGroup.mkdir(directories.split(path.sep), { recursive: true })
|
|
561
|
+
if (!group) {
|
|
562
|
+
throw new IOSWidgetTargetMutationError(`Failed to create Xcode group path for ${relativePath}`)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return group
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function ensureGroupContainsReference(group: PBXGroup, reference: PBXFileReference): void {
|
|
569
|
+
const alreadyPresent = group.props.children.some((child) => child.uuid === reference.uuid)
|
|
570
|
+
if (!alreadyPresent) {
|
|
571
|
+
group.props.children.push(reference)
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function removeGroupReference(group: PBXGroup, reference: PBXFileReference): void {
|
|
576
|
+
group.props.children = group.props.children.filter((child) => child.uuid !== reference.uuid)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function applyFileType(reference: PBXFileReference, relativePath: string): void {
|
|
580
|
+
const extension = path.extname(relativePath)
|
|
581
|
+
|
|
582
|
+
if (extension === '.swift') {
|
|
583
|
+
reference.setLastKnownFileType(SWIFT_FILE_TYPE)
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (extension === '.plist') {
|
|
588
|
+
reference.setLastKnownFileType(PLIST_FILE_TYPE)
|
|
589
|
+
return
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (extension === '.entitlements') {
|
|
593
|
+
reference.setLastKnownFileType('text.plist.entitlements')
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (extension === '.strings') {
|
|
598
|
+
reference.setLastKnownFileType(STRINGS_FILE_TYPE)
|
|
599
|
+
return
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (extension === '.xcassets') {
|
|
603
|
+
reference.setLastKnownFileType(ASSET_CATALOG_FILE_TYPE)
|
|
604
|
+
return
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function isSourceFile(relativePath: string): boolean {
|
|
609
|
+
return SOURCE_EXTENSIONS.has(path.extname(relativePath))
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function isResourceFile(relativePath: string): boolean {
|
|
613
|
+
const extension = path.extname(relativePath)
|
|
614
|
+
return RESOURCE_EXTENSIONS.has(extension) || relativePath.endsWith('.xcassets')
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function getBuildPhaseReferencePath(relativePath: string): string {
|
|
618
|
+
const normalizedPath = normalizeRelativePath(relativePath)
|
|
619
|
+
const assetCatalogIndex = normalizedPath.indexOf('/Assets.xcassets/')
|
|
620
|
+
|
|
621
|
+
if (assetCatalogIndex >= 0) {
|
|
622
|
+
return normalizedPath.slice(0, assetCatalogIndex + '/Assets.xcassets'.length)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return normalizedPath
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function getStaleReferencePaths(previousGeneratedFiles: string[], generatedFiles: string[]): Set<string> {
|
|
629
|
+
const currentReferencePaths = new Set(
|
|
630
|
+
generatedFiles.flatMap((file) => [getBuildPhaseReferencePath(file), getGroupReferencePath(file)])
|
|
631
|
+
)
|
|
632
|
+
const previousReferencePaths = new Set(
|
|
633
|
+
previousGeneratedFiles.flatMap((file) => [getBuildPhaseReferencePath(file), getGroupReferencePath(file)])
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
return new Set([...previousReferencePaths].filter((referencePath) => !currentReferencePaths.has(referencePath)))
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function getGroupReferencePath(relativePath: string): string {
|
|
640
|
+
const normalizedPath = normalizeRelativePath(relativePath)
|
|
641
|
+
const assetCatalogIndex = normalizedPath.indexOf('/Assets.xcassets/')
|
|
642
|
+
|
|
643
|
+
if (assetCatalogIndex >= 0) {
|
|
644
|
+
return normalizedPath.slice(0, assetCatalogIndex + '/Assets.xcassets'.length)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return normalizedPath
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function getReferenceRelativePath(context: IOSXcodeProjectContext, reference: PBXFileReference): string {
|
|
651
|
+
const segments = [stripQuotes(reference.props.path ?? '')].filter((segment) => segment.length > 0)
|
|
652
|
+
let parent = getPreferredParentGroup(reference)
|
|
653
|
+
|
|
654
|
+
while (parent && parent.uuid !== context.mainGroup.uuid) {
|
|
655
|
+
const parentPath = stripQuotes(parent.props.path ?? parent.props.name ?? '')
|
|
656
|
+
if (parentPath.length > 0) {
|
|
657
|
+
segments.unshift(parentPath)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
parent = getPreferredParentGroup(parent)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (segments.length === 0) {
|
|
664
|
+
throw new IOSWidgetTargetMutationError(`Unable to resolve Xcode file reference path for ${reference.uuid}`)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return normalizeRelativePath(segments.join('/'))
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function getPreferredParentGroup(object: PBXFileReference | PBXGroup): PBXGroup | undefined {
|
|
671
|
+
const parentGroups = object.getReferrers().filter((referrer): referrer is PBXGroup => PBXGroup.is(referrer))
|
|
672
|
+
|
|
673
|
+
if (parentGroups.length <= 1) {
|
|
674
|
+
return parentGroups[0]
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return [...parentGroups].sort((left, right) => getGroupSpecificity(right) - getGroupSpecificity(left))[0]
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function getGroupSpecificity(group: PBXGroup): number {
|
|
681
|
+
const identifier = group.props.path ?? group.props.name ?? group.getDisplayName()
|
|
682
|
+
|
|
683
|
+
if (identifier.endsWith('.imageset')) {
|
|
684
|
+
return 4
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (identifier.endsWith('.xcassets') || identifier.endsWith('.lproj')) {
|
|
688
|
+
return 3
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return 1
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function sanitizeWidgetGroupChildren(widgetGroup: PBXGroup): void {
|
|
695
|
+
const staleChildren = widgetGroup.props.children.filter((child) => {
|
|
696
|
+
const identifier = stripQuotes(
|
|
697
|
+
'path' in child && typeof child.props.path === 'string' ? child.props.path : child.getDisplayName()
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
if (identifier.endsWith('.imageset')) {
|
|
701
|
+
return true
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return PBXGroup.is(child) && identifier.endsWith('.xcassets')
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
widgetGroup.props.children = widgetGroup.props.children.filter((child) => !staleChildren.includes(child))
|
|
708
|
+
|
|
709
|
+
for (const child of staleChildren) {
|
|
710
|
+
if (PBXGroup.is(child)) {
|
|
711
|
+
removeGroupTree(child)
|
|
712
|
+
continue
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
child.removeFromProject()
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function removeGroupTree(group: PBXGroup): void {
|
|
720
|
+
const childGroups = [...group.getChildGroups()]
|
|
721
|
+
const childFiles = group.props.children.filter((child): child is PBXFileReference => PBXFileReference.is(child))
|
|
722
|
+
|
|
723
|
+
for (const childGroup of childGroups) {
|
|
724
|
+
removeGroupTree(childGroup)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
for (const childFile of childFiles) {
|
|
728
|
+
childFile.removeFromProject()
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
group.removeFromProject()
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function removeFileReferenceFromGroupTree(group: PBXGroup, reference: PBXFileReference): void {
|
|
735
|
+
group.props.children = group.props.children.filter((child) => child.uuid !== reference.uuid)
|
|
736
|
+
|
|
737
|
+
for (const childGroup of group.getChildGroups()) {
|
|
738
|
+
removeFileReferenceFromGroupTree(childGroup, reference)
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function getPathRelativeToTarget(relativePath: string, targetName: string): string | null {
|
|
743
|
+
const normalizedPath = normalizeRelativePath(relativePath)
|
|
744
|
+
if (!normalizedPath.startsWith(`${targetName}/`)) {
|
|
745
|
+
return null
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return normalizedPath.slice(targetName.length + 1)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function getStaleWidgetTargetNames(previousGeneratedFiles: string[], targetName: string): string[] {
|
|
752
|
+
return [
|
|
753
|
+
...new Set(
|
|
754
|
+
previousGeneratedFiles
|
|
755
|
+
.map(getWidgetTargetNameFromGeneratedPath)
|
|
756
|
+
.filter((candidate): candidate is string => candidate !== undefined && candidate !== targetName)
|
|
757
|
+
),
|
|
758
|
+
].sort()
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function getWidgetTargetNameFromGeneratedPath(relativePath: string): string | undefined {
|
|
762
|
+
const normalizedPath = normalizeRelativePath(relativePath)
|
|
763
|
+
const [targetName] = normalizedPath.split(path.sep, 1)
|
|
764
|
+
|
|
765
|
+
return typeof targetName === 'string' && targetName.length > 0 ? targetName : undefined
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function normalizeGeneratedFilePaths(
|
|
769
|
+
generatedFiles: string[],
|
|
770
|
+
projectRoot: string,
|
|
771
|
+
discovery: IOSProjectDiscovery
|
|
772
|
+
): string[] {
|
|
773
|
+
const iosRootRelativePath = normalizeRelativePath(path.relative(projectRoot, discovery.iosRoot))
|
|
774
|
+
const iosRootRelativePrefix = iosRootRelativePath === '.' ? '' : `${iosRootRelativePath}/`
|
|
775
|
+
|
|
776
|
+
return [
|
|
777
|
+
...new Set(generatedFiles.map((file) => toIOSProjectRelativePath(file, iosRootRelativePrefix)).filter(isDefined)),
|
|
778
|
+
].sort()
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function toIOSProjectRelativePath(relativeFilePath: string, iosRootRelativePrefix: string): string | undefined {
|
|
782
|
+
const normalizedPath = normalizeRelativePath(relativeFilePath)
|
|
783
|
+
|
|
784
|
+
if (iosRootRelativePrefix.length === 0) {
|
|
785
|
+
return normalizedPath
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (normalizedPath.startsWith(iosRootRelativePrefix)) {
|
|
789
|
+
return normalizedPath.slice(iosRootRelativePrefix.length)
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return undefined
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function resolveBundleIdentifier(
|
|
796
|
+
context: IOSXcodeProjectContext,
|
|
797
|
+
discovery: IOSProjectDiscovery,
|
|
798
|
+
targetName: string
|
|
799
|
+
): string {
|
|
800
|
+
const mainTargetBundleIdentifier =
|
|
801
|
+
context.mainAppTarget.buildConfigurations.default.resolveBuildSetting('PRODUCT_BUNDLE_IDENTIFIER')
|
|
802
|
+
|
|
803
|
+
if (typeof mainTargetBundleIdentifier !== 'string' || mainTargetBundleIdentifier.length === 0) {
|
|
804
|
+
throw new IOSWidgetTargetMutationError(
|
|
805
|
+
`Main app target '${discovery.mainTargetName}' is missing PRODUCT_BUNDLE_IDENTIFIER in ${discovery.pbxprojPath}`
|
|
806
|
+
)
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return `${stripQuotes(mainTargetBundleIdentifier)}.${sanitizeBundleIdentifierSegment(targetName)}`
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function sanitizeBundleIdentifierSegment(targetName: string): string {
|
|
813
|
+
const sanitized = targetName.replace(/[^A-Za-z0-9-]/g, '-')
|
|
814
|
+
return sanitized.replace(/-+/g, '-').replace(/^-+|-+$/g, '')
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function getMainAppCodeSigningSettings(context: IOSXcodeProjectContext): MainAppCodeSigningSettings {
|
|
818
|
+
const buildSettings = context.mainAppTarget.buildConfigurations.default.props.buildSettings ?? {}
|
|
819
|
+
|
|
820
|
+
return {
|
|
821
|
+
codeSignStyle: readBuildSettingString(buildSettings.CODE_SIGN_STYLE),
|
|
822
|
+
developmentTeam: readBuildSettingString(buildSettings.DEVELOPMENT_TEAM),
|
|
823
|
+
provisioningProfileSpecifier: readBuildSettingString(
|
|
824
|
+
(buildSettings as unknown as { PROVISIONING_PROFILE_SPECIFIER?: unknown }).PROVISIONING_PROFILE_SPECIFIER
|
|
825
|
+
),
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async function getMainAppEntitlementsBuildSetting(
|
|
830
|
+
discovery: IOSProjectDiscovery,
|
|
831
|
+
ios: NormalizedVoltraIOSConfig
|
|
832
|
+
): Promise<string | undefined> {
|
|
833
|
+
if (discovery.entitlementsPath) {
|
|
834
|
+
return normalizeRelativePath(path.relative(discovery.iosRoot, discovery.entitlementsPath))
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (!needsEntitlementsMutation(ios)) {
|
|
838
|
+
return undefined
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const entitlementsPath = resolveMainAppEntitlementsPath(discovery)
|
|
842
|
+
|
|
843
|
+
if (!(await pathExists(entitlementsPath))) {
|
|
844
|
+
return undefined
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return normalizeRelativePath(path.relative(discovery.iosRoot, entitlementsPath))
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function ensureMainAppEntitlementsBuildSetting(
|
|
851
|
+
context: IOSXcodeProjectContext,
|
|
852
|
+
entitlementsPath: string | undefined
|
|
853
|
+
): void {
|
|
854
|
+
for (const config of context.mainAppTarget.buildConfigurations.all) {
|
|
855
|
+
if (entitlementsPath) {
|
|
856
|
+
config.props.buildSettings.CODE_SIGN_ENTITLEMENTS = entitlementsPath
|
|
857
|
+
continue
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
delete config.props.buildSettings.CODE_SIGN_ENTITLEMENTS
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function ensureMainAppDeploymentTarget(context: IOSXcodeProjectContext): void {
|
|
865
|
+
for (const config of context.mainAppTarget.buildConfigurations.all) {
|
|
866
|
+
config.props.buildSettings.IPHONEOS_DEPLOYMENT_TARGET = resolveBuildConfigurationDeploymentTarget(
|
|
867
|
+
config,
|
|
868
|
+
VOLTRA_MIN_IOS_DEPLOYMENT_TARGET
|
|
869
|
+
)
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function resolveBuildConfigurationDeploymentTarget(
|
|
874
|
+
config: XCBuildConfiguration,
|
|
875
|
+
minimumDeploymentTarget: string
|
|
876
|
+
): string {
|
|
877
|
+
const currentDeploymentTarget = readBuildSettingString(config.props.buildSettings.IPHONEOS_DEPLOYMENT_TARGET)
|
|
878
|
+
return currentDeploymentTarget
|
|
879
|
+
? maxIOSDeploymentTarget(currentDeploymentTarget, minimumDeploymentTarget)
|
|
880
|
+
: minimumDeploymentTarget
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
interface MainAppCodeSigningSettings {
|
|
884
|
+
codeSignStyle?: string
|
|
885
|
+
developmentTeam?: string
|
|
886
|
+
provisioningProfileSpecifier?: string
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function readBuildSettingString(value: unknown): string | undefined {
|
|
890
|
+
return typeof value === 'string' ? stripQuotes(value) : undefined
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function stripQuotes(value: string | undefined): string {
|
|
894
|
+
return value?.replace(/^"|"$/g, '') ?? ''
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function isDefined<TValue>(value: TValue | undefined): value is TValue {
|
|
898
|
+
return value !== undefined
|
|
899
|
+
}
|