voltra 2.2.0 → 2.3.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.
Files changed (138) hide show
  1. package/build/cjs/apply/index.js +6 -1
  2. package/build/cjs/apply/index.js.map +1 -1
  3. package/build/cjs/config/normalize.js +150 -31
  4. package/build/cjs/config/normalize.js.map +1 -1
  5. package/build/cjs/config/perConfiguration.js +29 -0
  6. package/build/cjs/config/perConfiguration.js.map +1 -0
  7. package/build/cjs/config/serverUpdate.js +119 -0
  8. package/build/cjs/config/serverUpdate.js.map +1 -0
  9. package/build/cjs/config/widgetKind.js +25 -0
  10. package/build/cjs/config/widgetKind.js.map +1 -0
  11. package/build/cjs/dependencies/platformPackages.js +9 -0
  12. package/build/cjs/dependencies/platformPackages.js.map +1 -1
  13. package/build/cjs/discovery/ios.js +50 -31
  14. package/build/cjs/discovery/ios.js.map +1 -1
  15. package/build/cjs/discovery/metro.js +52 -0
  16. package/build/cjs/discovery/metro.js.map +1 -0
  17. package/build/cjs/index.js +6 -2
  18. package/build/cjs/index.js.map +1 -1
  19. package/build/cjs/platforms/android/generated.js +98 -40
  20. package/build/cjs/platforms/android/generated.js.map +1 -1
  21. package/build/cjs/platforms/android/widgetSizing.js +89 -0
  22. package/build/cjs/platforms/android/widgetSizing.js.map +1 -0
  23. package/build/cjs/platforms/ios/apply.js +21 -11
  24. package/build/cjs/platforms/ios/apply.js.map +1 -1
  25. package/build/cjs/platforms/ios/buildConfigurationValues.js +73 -0
  26. package/build/cjs/platforms/ios/buildConfigurationValues.js.map +1 -0
  27. package/build/cjs/platforms/ios/entitlements.js +34 -19
  28. package/build/cjs/platforms/ios/entitlements.js.map +1 -1
  29. package/build/cjs/platforms/ios/generated.js +79 -32
  30. package/build/cjs/platforms/ios/generated.js.map +1 -1
  31. package/build/cjs/platforms/ios/plist.js +22 -4
  32. package/build/cjs/platforms/ios/plist.js.map +1 -1
  33. package/build/cjs/platforms/ios/targetName.js.map +1 -1
  34. package/build/cjs/platforms/ios/xcodeTarget.js +166 -23
  35. package/build/cjs/platforms/ios/xcodeTarget.js.map +1 -1
  36. package/build/cjs/platforms/shared/widgetModule.js +18 -145
  37. package/build/cjs/platforms/shared/widgetModule.js.map +1 -1
  38. package/build/esm/apply/index.js +6 -1
  39. package/build/esm/apply/index.js.map +1 -1
  40. package/build/esm/config/normalize.js +150 -31
  41. package/build/esm/config/normalize.js.map +1 -1
  42. package/build/esm/config/perConfiguration.js +25 -0
  43. package/build/esm/config/perConfiguration.js.map +1 -0
  44. package/build/esm/config/serverUpdate.js +112 -0
  45. package/build/esm/config/serverUpdate.js.map +1 -0
  46. package/build/esm/config/widgetKind.js +20 -0
  47. package/build/esm/config/widgetKind.js.map +1 -0
  48. package/build/esm/dependencies/platformPackages.js +8 -0
  49. package/build/esm/dependencies/platformPackages.js.map +1 -1
  50. package/build/esm/discovery/ios.js +50 -31
  51. package/build/esm/discovery/ios.js.map +1 -1
  52. package/build/esm/discovery/metro.js +46 -0
  53. package/build/esm/discovery/metro.js.map +1 -0
  54. package/build/esm/index.js +1 -0
  55. package/build/esm/index.js.map +1 -1
  56. package/build/esm/platforms/android/generated.js +100 -42
  57. package/build/esm/platforms/android/generated.js.map +1 -1
  58. package/build/esm/platforms/android/widgetSizing.js +85 -0
  59. package/build/esm/platforms/android/widgetSizing.js.map +1 -0
  60. package/build/esm/platforms/ios/apply.js +21 -11
  61. package/build/esm/platforms/ios/apply.js.map +1 -1
  62. package/build/esm/platforms/ios/buildConfigurationValues.js +68 -0
  63. package/build/esm/platforms/ios/buildConfigurationValues.js.map +1 -0
  64. package/build/esm/platforms/ios/entitlements.js +34 -19
  65. package/build/esm/platforms/ios/entitlements.js.map +1 -1
  66. package/build/esm/platforms/ios/generated.js +80 -33
  67. package/build/esm/platforms/ios/generated.js.map +1 -1
  68. package/build/esm/platforms/ios/plist.js +22 -4
  69. package/build/esm/platforms/ios/plist.js.map +1 -1
  70. package/build/esm/platforms/ios/targetName.js.map +1 -1
  71. package/build/esm/platforms/ios/xcodeTarget.js +166 -23
  72. package/build/esm/platforms/ios/xcodeTarget.js.map +1 -1
  73. package/build/esm/platforms/shared/widgetModule.js +16 -107
  74. package/build/esm/platforms/shared/widgetModule.js.map +1 -1
  75. package/build/types/apply/index.d.ts.map +1 -1
  76. package/build/types/config/normalize.d.ts.map +1 -1
  77. package/build/types/config/perConfiguration.d.ts +18 -0
  78. package/build/types/config/perConfiguration.d.ts.map +1 -0
  79. package/build/types/config/serverUpdate.d.ts +80 -0
  80. package/build/types/config/serverUpdate.d.ts.map +1 -0
  81. package/build/types/config/types.d.ts +91 -30
  82. package/build/types/config/types.d.ts.map +1 -1
  83. package/build/types/config/widgetKind.d.ts +11 -0
  84. package/build/types/config/widgetKind.d.ts.map +1 -0
  85. package/build/types/dependencies/platformPackages.d.ts +1 -0
  86. package/build/types/dependencies/platformPackages.d.ts.map +1 -1
  87. package/build/types/discovery/ios.d.ts +10 -0
  88. package/build/types/discovery/ios.d.ts.map +1 -1
  89. package/build/types/discovery/metro.d.ts +21 -0
  90. package/build/types/discovery/metro.d.ts.map +1 -0
  91. package/build/types/index.d.ts +3 -1
  92. package/build/types/index.d.ts.map +1 -1
  93. package/build/types/platforms/android/generated.d.ts.map +1 -1
  94. package/build/types/platforms/android/widgetSizing.d.ts +50 -0
  95. package/build/types/platforms/android/widgetSizing.d.ts.map +1 -0
  96. package/build/types/platforms/ios/apply.d.ts.map +1 -1
  97. package/build/types/platforms/ios/buildConfigurationValues.d.ts +33 -0
  98. package/build/types/platforms/ios/buildConfigurationValues.d.ts.map +1 -0
  99. package/build/types/platforms/ios/entitlements.d.ts +4 -4
  100. package/build/types/platforms/ios/entitlements.d.ts.map +1 -1
  101. package/build/types/platforms/ios/generated.d.ts +14 -2
  102. package/build/types/platforms/ios/generated.d.ts.map +1 -1
  103. package/build/types/platforms/ios/plist.d.ts +3 -3
  104. package/build/types/platforms/ios/plist.d.ts.map +1 -1
  105. package/build/types/platforms/ios/podfile.d.ts +2 -2
  106. package/build/types/platforms/ios/podfile.d.ts.map +1 -1
  107. package/build/types/platforms/ios/targetName.d.ts +2 -2
  108. package/build/types/platforms/ios/targetName.d.ts.map +1 -1
  109. package/build/types/platforms/ios/xcodeTarget.d.ts +4 -2
  110. package/build/types/platforms/ios/xcodeTarget.d.ts.map +1 -1
  111. package/build/types/platforms/shared/widgetModule.d.ts +12 -3
  112. package/build/types/platforms/shared/widgetModule.d.ts.map +1 -1
  113. package/package.json +5 -5
  114. package/src/apply/index.ts +6 -1
  115. package/src/config/normalize.node.test.ts +105 -0
  116. package/src/config/normalize.ts +217 -45
  117. package/src/config/perConfiguration.ts +49 -0
  118. package/src/config/serverUpdate.ts +160 -0
  119. package/src/config/types.ts +97 -30
  120. package/src/config/widgetKind.node.test.ts +27 -0
  121. package/src/config/widgetKind.ts +27 -0
  122. package/src/dependencies/platformPackages.ts +11 -0
  123. package/src/discovery/ios.ts +104 -60
  124. package/src/discovery/metro.node.test.ts +91 -0
  125. package/src/discovery/metro.ts +54 -0
  126. package/src/index.ts +8 -0
  127. package/src/platforms/android/generated.ts +144 -47
  128. package/src/platforms/android/widgetSizing.ts +112 -0
  129. package/src/platforms/ios/apply.ts +21 -11
  130. package/src/platforms/ios/buildConfigurationValues.ts +105 -0
  131. package/src/platforms/ios/entitlements.ts +55 -31
  132. package/src/platforms/ios/generated.node.test.ts +61 -0
  133. package/src/platforms/ios/generated.ts +137 -38
  134. package/src/platforms/ios/plist.ts +36 -7
  135. package/src/platforms/ios/podfile.ts +2 -2
  136. package/src/platforms/ios/targetName.ts +2 -2
  137. package/src/platforms/ios/xcodeTarget.ts +248 -43
  138. package/src/platforms/shared/widgetModule.ts +22 -140
@@ -1,11 +1,12 @@
1
1
  import { parseStringPromise } from 'xml2js'
2
2
 
3
+ import { iosWidgetKindOverrides } from '../../config/widgetKind'
3
4
  import { readTextFile, writeTextFile } from '../../fs/readWrite'
4
5
  import { toRelativePath } from '../../fs/path'
5
6
  import { VoltraCliError } from '../../reporting/summary'
6
7
 
7
8
  import type { IOSProjectDiscovery } from '../../discovery/ios'
8
- import type { NormalizedVoltraIOSConfig } from '../../config/types'
9
+ import type { ResolvedVoltraIOSConfig } from '../../config/types'
9
10
  import type { ReportedChange } from '../../reporting/summary'
10
11
 
11
12
  interface OrderedPlistNode {
@@ -23,12 +24,12 @@ type PlistErrorFactory = (message: string) => Error
23
24
 
24
25
  export interface EnsureInfoPlistOptions {
25
26
  projectRoot: string
26
- ios: NormalizedVoltraIOSConfig
27
+ ios: ResolvedVoltraIOSConfig
27
28
  discovery: IOSProjectDiscovery
28
29
  }
29
30
 
30
31
  export interface EnsureInfoPlistResult {
31
- change?: ReportedChange
32
+ changes: ReportedChange[]
32
33
  }
33
34
 
34
35
  export class IOSInfoPlistMutationError extends VoltraCliError {
@@ -40,7 +41,26 @@ export class IOSInfoPlistMutationError extends VoltraCliError {
40
41
 
41
42
  export async function ensureInfoPlist(options: EnsureInfoPlistOptions): Promise<EnsureInfoPlistResult> {
42
43
  const { projectRoot, ios, discovery } = options
43
- const infoPlist = await parsePlistFile(discovery.infoPlistPath, 'main app Info.plist', createInfoPlistError)
44
+ const changes: ReportedChange[] = []
45
+
46
+ // Build configurations of a multi-environment app can each reference their own Info.plist.
47
+ for (const infoPlistPath of discovery.infoPlistPaths) {
48
+ const change = await ensureSingleInfoPlist(projectRoot, ios, infoPlistPath)
49
+
50
+ if (change) {
51
+ changes.push(change)
52
+ }
53
+ }
54
+
55
+ return { changes }
56
+ }
57
+
58
+ async function ensureSingleInfoPlist(
59
+ projectRoot: string,
60
+ ios: ResolvedVoltraIOSConfig,
61
+ infoPlistPath: string
62
+ ): Promise<ReportedChange | undefined> {
63
+ const infoPlist = await parsePlistFile(infoPlistPath, 'main app Info.plist', createInfoPlistError)
44
64
 
45
65
  infoPlist.NSSupportsLiveActivities = true
46
66
  infoPlist.NSSupportsLiveActivitiesFrequentUpdates = false
@@ -52,8 +72,18 @@ export async function ensureInfoPlist(options: EnsureInfoPlistOptions): Promise<
52
72
  const widgetIds = ios.widgets.map((widget) => widget.id)
53
73
  setOrDeleteVoltraKey(infoPlist, 'Voltra_WidgetIds', widgetIds.length > 0 ? widgetIds : undefined)
54
74
 
75
+ // Widgets that pin a custom WidgetKit kind, so VoltraWidgetKind can map ids to kinds and back.
76
+ setOrDeleteVoltraKey(infoPlist, 'Voltra_WidgetKinds', iosWidgetKindOverrides(ios.widgets))
77
+
78
+ // Every server-driven widget gets an interval, so this dictionary's keys are the set of
79
+ // server-driven widget ids the runtime settings store validates against. A URL is written only
80
+ // when app.json set one; otherwise the app supplies it with setWidgetServerUpdate.
55
81
  const serverWidgets = ios.widgets.filter((widget) => widget.serverUpdate)
56
- const serverUrls = Object.fromEntries(serverWidgets.map((widget) => [widget.id, widget.serverUpdate?.url]))
82
+ const serverUrls = Object.fromEntries(
83
+ serverWidgets
84
+ .filter((widget) => widget.serverUpdate?.url !== undefined)
85
+ .map((widget) => [widget.id, widget.serverUpdate?.url])
86
+ )
57
87
  const serverIntervals = Object.fromEntries(
58
88
  serverWidgets.map((widget) => [widget.id, widget.serverUpdate?.intervalMinutes])
59
89
  )
@@ -70,9 +100,8 @@ export async function ensureInfoPlist(options: EnsureInfoPlistOptions): Promise<
70
100
  )
71
101
 
72
102
  const nextContent = buildPlistXml(infoPlist, createInfoPlistError)
73
- const change = await writePlistIfChanged(projectRoot, discovery.infoPlistPath, nextContent)
74
103
 
75
- return { change }
104
+ return await writePlistIfChanged(projectRoot, infoPlistPath, nextContent)
76
105
  }
77
106
 
78
107
  export async function parsePlistFile(
@@ -8,7 +8,7 @@ import { resolveIOSWidgetTargetName } from './targetName'
8
8
  import { VOLTRA_MIN_IOS_DEPLOYMENT_TARGET, isIOSDeploymentTargetAtLeast } from './deploymentTarget'
9
9
 
10
10
  import type { IOSProjectDiscovery } from '../../discovery/ios'
11
- import type { NormalizedVoltraIOSConfig } from '../../config/types'
11
+ import type { ResolvedVoltraIOSConfig } from '../../config/types'
12
12
  import type { ReportedChange } from '../../reporting/summary'
13
13
 
14
14
  const VOLTRA_MANAGED_BLOCK_START = '# >>> VOLTRA MANAGED BLOCK: widget extension target'
@@ -18,7 +18,7 @@ const LEGACY_BLOCK_NOTICE = '# DO NOT MODIFY THIS FILE - IT IS AUTO-GENERATED BY
18
18
 
19
19
  export interface EnsurePodfileBlockOptions {
20
20
  projectRoot: string
21
- ios: NormalizedVoltraIOSConfig
21
+ ios: ResolvedVoltraIOSConfig
22
22
  discovery: IOSProjectDiscovery
23
23
  }
24
24
 
@@ -1,7 +1,7 @@
1
1
  import type { IOSProjectDiscovery } from '../../discovery/ios'
2
- import type { NormalizedVoltraIOSConfig } from '../../config/types'
2
+ import type { ResolvedVoltraIOSConfig } from '../../config/types'
3
3
 
4
- export function resolveIOSWidgetTargetName(ios: NormalizedVoltraIOSConfig, discovery: IOSProjectDiscovery): string {
4
+ export function resolveIOSWidgetTargetName(ios: ResolvedVoltraIOSConfig, discovery: IOSProjectDiscovery): string {
5
5
  if (ios.targetName) {
6
6
  return ios.targetName
7
7
  }
@@ -19,15 +19,21 @@ import { resolveIOSWidgetTargetName } from './targetName'
19
19
  import { ensureMainGroupChild, openIOSXcodeProject, saveIOSXcodeProject } from './xcode'
20
20
  import { resolveMainAppEntitlementsPath } from './mainAppEntitlements'
21
21
  import { needsEntitlementsMutation } from './entitlements'
22
+ import { VOLTRA_OWNED_BUILD_SETTINGS } from './buildConfigurationValues'
22
23
  import { VOLTRA_MIN_IOS_DEPLOYMENT_TARGET, maxIOSDeploymentTarget } from './deploymentTarget'
23
24
 
24
25
  import type { IOSProjectDiscovery } from '../../discovery/ios'
25
- import type { NormalizedVoltraIOSConfig } from '../../config/types'
26
+ import type { ResolvedVoltraIOSConfig } from '../../config/types'
26
27
  import type { ReportedChange } from '../../reporting/summary'
27
28
  import type { IOSXcodeProjectContext } from './xcode'
28
29
  import type { BuildSettings } from '@bacons/xcode/build/json/types'
29
30
 
30
31
  const IOS_APP_EXTENSION_PRODUCT_TYPE = 'com.apple.product-type.app-extension'
32
+ const OPTIONAL_WIDGET_CODE_SIGNING_SETTINGS = [
33
+ 'CODE_SIGN_STYLE',
34
+ 'DEVELOPMENT_TEAM',
35
+ 'PROVISIONING_PROFILE_SPECIFIER',
36
+ ] as const
31
37
  const PRODUCT_FILE_TYPE = 'wrapper.app-extension'
32
38
  const SWIFT_FILE_TYPE = 'sourcecode.swift'
33
39
  const STRINGS_FILE_TYPE = 'text.plist.strings'
@@ -84,10 +90,12 @@ echo "Voltra: widget bundler resolved to $BUNDLER"
84
90
 
85
91
  export interface EnsureIOSWidgetTargetOptions {
86
92
  projectRoot: string
87
- ios: NormalizedVoltraIOSConfig
93
+ ios: ResolvedVoltraIOSConfig
88
94
  discovery: IOSProjectDiscovery
89
95
  generatedFiles: string[]
90
96
  previousGeneratedFiles?: string[]
97
+ /** User-defined build settings to write at project level, keyed by build configuration name. */
98
+ buildSettings?: Map<string, Record<string, string>>
91
99
  }
92
100
 
93
101
  export interface EnsureIOSWidgetTargetResult {
@@ -105,7 +113,7 @@ export class IOSWidgetTargetMutationError extends VoltraCliError {
105
113
  export async function ensureIOSWidgetTarget(
106
114
  options: EnsureIOSWidgetTargetOptions
107
115
  ): Promise<EnsureIOSWidgetTargetResult> {
108
- const { projectRoot, ios, discovery, generatedFiles, previousGeneratedFiles } = options
116
+ const { projectRoot, ios, discovery, generatedFiles, previousGeneratedFiles, buildSettings } = options
109
117
  const targetName = resolveIOSWidgetTargetName(ios, discovery)
110
118
  const context = openIOSXcodeProject(discovery)
111
119
  const beforeSerialized = JSON.stringify(context.project.toJSON())
@@ -113,16 +121,22 @@ export async function ensureIOSWidgetTarget(
113
121
  const nextGeneratedFiles = normalizeGeneratedFilePaths(generatedFiles, projectRoot, discovery)
114
122
  const previousWidgetFiles = normalizeGeneratedFilePaths(previousGeneratedFiles ?? [], projectRoot, discovery)
115
123
  const staleTargetNames = getStaleWidgetTargetNames(previousWidgetFiles, targetName)
116
- const bundleIdentifier = resolveBundleIdentifier(context, discovery, targetName)
117
- const codeSigning = getMainAppCodeSigningSettings(context)
124
+ const mainAppSettings = getMainAppConfigurationSettings(context, discovery, targetName)
118
125
  const mainAppEntitlementsPath = await getMainAppEntitlementsBuildSetting(discovery, ios)
119
126
 
120
127
  removeStaleWidgetTargets(context, staleTargetNames)
121
- ensureMainAppEntitlementsBuildSetting(context, mainAppEntitlementsPath)
128
+ ensureMainAppEntitlementsBuildSetting(
129
+ context,
130
+ mainAppEntitlementsPath,
131
+ ios.project.entitlementsPath !== undefined,
132
+ getEntitlementsPathByConfiguration(discovery)
133
+ )
122
134
  ensureMainAppDeploymentTarget(context)
123
- ensureWidgetTarget(context, targetName, bundleIdentifier, ios.deploymentTarget, codeSigning)
135
+ ensureWidgetTarget(context, targetName, ios.deploymentTarget, mainAppSettings)
124
136
 
125
137
  const widgetTarget = getWidgetTarget(context, targetName)
138
+
139
+ ensureVoltraBuildSettings(context, widgetTarget, buildSettings ?? new Map())
126
140
  const widgetGroup = ensureWidgetGroup(context, targetName)
127
141
  const productFile = ensureProductFile(context, targetName, productPath)
128
142
 
@@ -159,24 +173,17 @@ export async function ensureIOSWidgetTarget(
159
173
  function ensureWidgetTarget(
160
174
  context: IOSXcodeProjectContext,
161
175
  targetName: string,
162
- bundleIdentifier: string,
163
176
  deploymentTarget: string,
164
- codeSigning: MainAppCodeSigningSettings
177
+ mainAppSettings: MainAppSettingsByConfiguration
165
178
  ): PBXNativeTarget {
166
179
  const existingTarget = getWidgetTargetOptional(context, targetName)
167
180
 
168
181
  if (existingTarget) {
169
- ensureBuildConfigurations(existingTarget, targetName, bundleIdentifier, deploymentTarget, codeSigning)
182
+ ensureBuildConfigurations(context, existingTarget, targetName, deploymentTarget, mainAppSettings)
170
183
  return existingTarget
171
184
  }
172
185
 
173
- const buildConfigurationList = createBuildConfigurationList(
174
- context,
175
- targetName,
176
- bundleIdentifier,
177
- deploymentTarget,
178
- codeSigning
179
- )
186
+ const buildConfigurationList = createBuildConfigurationList(context, targetName, deploymentTarget, mainAppSettings)
180
187
  const target = context.project.rootObject.createNativeTarget({
181
188
  buildConfigurationList,
182
189
  name: targetName,
@@ -193,19 +200,17 @@ function ensureWidgetTarget(
193
200
  function createBuildConfigurationList(
194
201
  context: IOSXcodeProjectContext,
195
202
  targetName: string,
196
- bundleIdentifier: string,
197
203
  deploymentTarget: string,
198
- codeSigning: MainAppCodeSigningSettings
204
+ mainAppSettings: MainAppSettingsByConfiguration
199
205
  ): XCConfigurationList {
200
206
  const configs = context.mainAppTarget.buildConfigurations.all.map((config) => {
201
207
  return XCBuildConfiguration.create(context.project, {
202
208
  name: config.props.name,
203
209
  buildSettings: buildWidgetBuildSettings(
204
210
  targetName,
205
- bundleIdentifier,
206
211
  deploymentTarget,
207
- codeSigning,
208
- config.props.name
212
+ config.props.name,
213
+ getMainAppSettingsFor(mainAppSettings, config.props.name)
209
214
  ),
210
215
  })
211
216
  })
@@ -217,11 +222,11 @@ function createBuildConfigurationList(
217
222
  }
218
223
 
219
224
  function ensureBuildConfigurations(
225
+ context: IOSXcodeProjectContext,
220
226
  target: PBXNativeTarget,
221
227
  targetName: string,
222
- bundleIdentifier: string,
223
228
  minimumDeploymentTarget: string,
224
- codeSigning: MainAppCodeSigningSettings
229
+ mainAppSettings: MainAppSettingsByConfiguration
225
230
  ): void {
226
231
  const configurationList = target.props.buildConfigurationList
227
232
 
@@ -231,23 +236,71 @@ function ensureBuildConfigurations(
231
236
  )
232
237
  }
233
238
 
239
+ addMissingWidgetBuildConfigurations(context, configurationList, targetName, minimumDeploymentTarget, mainAppSettings)
240
+
234
241
  for (const config of configurationList.props.buildConfigurations) {
235
242
  const deploymentTarget = resolveBuildConfigurationDeploymentTarget(config, minimumDeploymentTarget)
236
243
 
237
- Object.assign(
238
- config.props.buildSettings,
239
- buildWidgetBuildSettings(targetName, bundleIdentifier, deploymentTarget, codeSigning, config.props.name)
244
+ const nextBuildSettings = buildWidgetBuildSettings(
245
+ targetName,
246
+ deploymentTarget,
247
+ config.props.name,
248
+ getMainAppSettingsFor(mainAppSettings, config.props.name)
249
+ )
250
+
251
+ Object.assign(config.props.buildSettings, nextBuildSettings)
252
+
253
+ // Signing settings the app configuration no longer sets are dropped rather than left behind,
254
+ // now that each configuration can change independently of the default one.
255
+ for (const settingName of OPTIONAL_WIDGET_CODE_SIGNING_SETTINGS) {
256
+ if (nextBuildSettings[settingName] === undefined) {
257
+ delete (config.props.buildSettings as unknown as Record<string, unknown>)[settingName]
258
+ }
259
+ }
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Mirrors build configurations added to the app since the widget target was created, so an
265
+ * environment added later still builds the extension.
266
+ */
267
+ function addMissingWidgetBuildConfigurations(
268
+ context: IOSXcodeProjectContext,
269
+ configurationList: XCConfigurationList,
270
+ targetName: string,
271
+ minimumDeploymentTarget: string,
272
+ mainAppSettings: MainAppSettingsByConfiguration
273
+ ): void {
274
+ const existingNames = new Set(configurationList.props.buildConfigurations.map((config) => config.props.name))
275
+
276
+ for (const config of context.mainAppTarget.buildConfigurations.all) {
277
+ const configurationName = config.props.name
278
+
279
+ if (existingNames.has(configurationName)) {
280
+ continue
281
+ }
282
+
283
+ configurationList.props.buildConfigurations.push(
284
+ XCBuildConfiguration.create(context.project, {
285
+ name: configurationName,
286
+ buildSettings: buildWidgetBuildSettings(
287
+ targetName,
288
+ resolveBuildConfigurationDeploymentTarget(config, minimumDeploymentTarget),
289
+ configurationName,
290
+ getMainAppSettingsFor(mainAppSettings, configurationName)
291
+ ),
292
+ })
240
293
  )
241
294
  }
242
295
  }
243
296
 
244
297
  function buildWidgetBuildSettings(
245
298
  targetName: string,
246
- bundleIdentifier: string,
247
299
  deploymentTarget: string,
248
- codeSigning: MainAppCodeSigningSettings,
249
- configurationName: string
300
+ configurationName: string,
301
+ mainAppSettings: MainAppConfigurationSettings
250
302
  ): BuildSettings & Record<string, string | undefined> {
303
+ const { bundleIdentifier, codeSigning } = mainAppSettings
251
304
  const buildSettings: BuildSettings & Record<string, string | undefined> = {
252
305
  ASSETCATALOG_COMPILER_APPICON_NAME: '',
253
306
  CODE_SIGN_ENTITLEMENTS: `${targetName}/${targetName}.entitlements`,
@@ -792,21 +845,106 @@ function toIOSProjectRelativePath(relativeFilePath: string, iosRootRelativePrefi
792
845
  return undefined
793
846
  }
794
847
 
795
- function resolveBundleIdentifier(
848
+ /**
849
+ * Widget settings are derived per build configuration: apps that ship several environments give each
850
+ * configuration its own bundle identifier and signing setup, and the extension has to match the app
851
+ * it is embedded in for every one of them.
852
+ */
853
+ function getMainAppConfigurationSettings(
796
854
  context: IOSXcodeProjectContext,
797
855
  discovery: IOSProjectDiscovery,
798
856
  targetName: string
799
- ): string {
800
- const mainTargetBundleIdentifier =
801
- context.mainAppTarget.buildConfigurations.default.resolveBuildSetting('PRODUCT_BUNDLE_IDENTIFIER')
857
+ ): MainAppSettingsByConfiguration {
858
+ const defaultConfiguration = context.mainAppTarget.buildConfigurations.default
859
+ const defaultBundleIdentifier = readMainAppBundleIdentifier(defaultConfiguration)
802
860
 
803
- if (typeof mainTargetBundleIdentifier !== 'string' || mainTargetBundleIdentifier.length === 0) {
861
+ if (!defaultBundleIdentifier) {
804
862
  throw new IOSWidgetTargetMutationError(
805
863
  `Main app target '${discovery.mainTargetName}' is missing PRODUCT_BUNDLE_IDENTIFIER in ${discovery.pbxprojPath}`
806
864
  )
807
865
  }
808
866
 
809
- return `${stripQuotes(mainTargetBundleIdentifier)}.${sanitizeBundleIdentifierSegment(targetName)}`
867
+ const byConfigurationName = new Map<string, MainAppConfigurationSettings>()
868
+
869
+ for (const config of context.mainAppTarget.buildConfigurations.all) {
870
+ const bundleIdentifier = readMainAppBundleIdentifier(config) ?? defaultBundleIdentifier
871
+
872
+ byConfigurationName.set(config.props.name, {
873
+ // The raw value is kept, so an app identifier built from build settings keeps expanding.
874
+ bundleIdentifier: `${bundleIdentifier}.${sanitizeBundleIdentifierSegment(targetName)}`,
875
+ codeSigning: getCodeSigningSettings(config),
876
+ })
877
+ }
878
+
879
+ return {
880
+ byConfigurationName,
881
+ fallback: {
882
+ bundleIdentifier: `${defaultBundleIdentifier}.${sanitizeBundleIdentifierSegment(targetName)}`,
883
+ codeSigning: getCodeSigningSettings(defaultConfiguration),
884
+ },
885
+ }
886
+ }
887
+
888
+ function getMainAppSettingsFor(
889
+ mainAppSettings: MainAppSettingsByConfiguration,
890
+ configurationName: string
891
+ ): MainAppConfigurationSettings {
892
+ return mainAppSettings.byConfigurationName.get(configurationName) ?? mainAppSettings.fallback
893
+ }
894
+
895
+ function readMainAppBundleIdentifier(config: XCBuildConfiguration): string | undefined {
896
+ const rawBundleIdentifier = readBuildSettingString(config.props.buildSettings?.PRODUCT_BUNDLE_IDENTIFIER)
897
+
898
+ if (rawBundleIdentifier) {
899
+ return substituteTargetScopedBuildSettings(rawBundleIdentifier, config)
900
+ }
901
+
902
+ const inheritedBundleIdentifier = config.resolveBuildSetting('PRODUCT_BUNDLE_IDENTIFIER')
903
+
904
+ if (typeof inheritedBundleIdentifier !== 'string' || inheritedBundleIdentifier.length === 0) {
905
+ return undefined
906
+ }
907
+
908
+ return stripQuotes(inheritedBundleIdentifier)
909
+ }
910
+
911
+ /**
912
+ * Substitutes the build settings whose value depends on the target being built, leaving every other
913
+ * reference in place.
914
+ *
915
+ * An app identifier is routinely assembled from build settings. Those that mean something different
916
+ * in the widget target have to be resolved against the app now — the React Native template's
917
+ * `org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)` would otherwise name the widget
918
+ * after itself and stop being a child of the app's identifier. Everything else is left unexpanded,
919
+ * so a setting the project defines per build configuration, such as `$(BUNDLE_SUFFIX)`, keeps
920
+ * expanding per build in the widget target too.
921
+ */
922
+ function substituteTargetScopedBuildSettings(value: string, config: XCBuildConfiguration): string {
923
+ return value.replace(/\$\((PRODUCT_NAME|TARGET_NAME)(?::([A-Za-z0-9]+))?\)/g, (match, settingName, modifier) => {
924
+ const settingValue = config.resolveBuildSetting(settingName)
925
+
926
+ if (typeof settingValue !== 'string' || settingValue.length === 0) {
927
+ return match
928
+ }
929
+
930
+ return applyBuildSettingModifier(stripQuotes(settingValue), modifier) ?? match
931
+ })
932
+ }
933
+
934
+ function applyBuildSettingModifier(value: string, modifier: string | undefined): string | undefined {
935
+ switch (modifier) {
936
+ case undefined:
937
+ return value
938
+ case 'rfc1034identifier':
939
+ return value.replace(/[^a-zA-Z0-9]/g, '-')
940
+ case 'lower':
941
+ return value.toLowerCase()
942
+ case 'upper':
943
+ return value.toUpperCase()
944
+ default:
945
+ // An unsupported modifier is left for Xcode rather than guessed at.
946
+ return undefined
947
+ }
810
948
  }
811
949
 
812
950
  function sanitizeBundleIdentifierSegment(targetName: string): string {
@@ -814,8 +952,8 @@ function sanitizeBundleIdentifierSegment(targetName: string): string {
814
952
  return sanitized.replace(/-+/g, '-').replace(/^-+|-+$/g, '')
815
953
  }
816
954
 
817
- function getMainAppCodeSigningSettings(context: IOSXcodeProjectContext): MainAppCodeSigningSettings {
818
- const buildSettings = context.mainAppTarget.buildConfigurations.default.props.buildSettings ?? {}
955
+ function getCodeSigningSettings(config: XCBuildConfiguration): MainAppCodeSigningSettings {
956
+ const buildSettings = config.props.buildSettings ?? {}
819
957
 
820
958
  return {
821
959
  codeSignStyle: readBuildSettingString(buildSettings.CODE_SIGN_STYLE),
@@ -828,7 +966,7 @@ function getMainAppCodeSigningSettings(context: IOSXcodeProjectContext): MainApp
828
966
 
829
967
  async function getMainAppEntitlementsBuildSetting(
830
968
  discovery: IOSProjectDiscovery,
831
- ios: NormalizedVoltraIOSConfig
969
+ ios: ResolvedVoltraIOSConfig
832
970
  ): Promise<string | undefined> {
833
971
  if (discovery.entitlementsPath) {
834
972
  return normalizeRelativePath(path.relative(discovery.iosRoot, discovery.entitlementsPath))
@@ -847,17 +985,73 @@ async function getMainAppEntitlementsBuildSetting(
847
985
  return normalizeRelativePath(path.relative(discovery.iosRoot, entitlementsPath))
848
986
  }
849
987
 
988
+ /**
989
+ * Writes the user-defined build settings that hold Voltra's per-build-configuration values.
990
+ *
991
+ * They are set on the app and widget targets rather than on the project, so the build
992
+ * configurations written to are exactly the ones the values were validated against: a target can
993
+ * carry a configuration the project-level list does not have, and a value silently dropped there
994
+ * would expand to an empty App Group at build time. Settings Voltra no longer defines are removed.
995
+ */
996
+ function ensureVoltraBuildSettings(
997
+ context: IOSXcodeProjectContext,
998
+ widgetTarget: PBXNativeTarget,
999
+ buildSettings: Map<string, Record<string, string>>
1000
+ ): void {
1001
+ const configurations = [
1002
+ ...context.mainAppTarget.buildConfigurations.all,
1003
+ ...(widgetTarget.props.buildConfigurationList?.props.buildConfigurations ?? []),
1004
+ ]
1005
+
1006
+ for (const config of configurations) {
1007
+ const configurationSettings = config.props.buildSettings as unknown as Record<string, string | undefined>
1008
+ const nextSettings = buildSettings.get(config.props.name) ?? {}
1009
+
1010
+ for (const settingName of VOLTRA_OWNED_BUILD_SETTINGS) {
1011
+ if (nextSettings[settingName] === undefined) {
1012
+ delete configurationSettings[settingName]
1013
+ }
1014
+ }
1015
+
1016
+ Object.assign(configurationSettings, nextSettings)
1017
+ }
1018
+ }
1019
+
1020
+ function getEntitlementsPathByConfiguration(discovery: IOSProjectDiscovery): Map<string, string> {
1021
+ return new Map(
1022
+ [...(discovery.entitlementsPathByConfiguration ?? [])].map(([buildConfigurationName, entitlementsPath]) => [
1023
+ buildConfigurationName,
1024
+ normalizeRelativePath(path.relative(discovery.iosRoot, entitlementsPath)),
1025
+ ])
1026
+ )
1027
+ }
1028
+
850
1029
  function ensureMainAppEntitlementsBuildSetting(
851
1030
  context: IOSXcodeProjectContext,
852
- entitlementsPath: string | undefined
1031
+ entitlementsPath: string | undefined,
1032
+ isConfiguredExplicitly: boolean,
1033
+ entitlementsPathByConfiguration: Map<string, string>
853
1034
  ): void {
854
1035
  for (const config of context.mainAppTarget.buildConfigurations.all) {
855
- if (entitlementsPath) {
856
- config.props.buildSettings.CODE_SIGN_ENTITLEMENTS = entitlementsPath
1036
+ const configuredEntitlementsPath = entitlementsPathByConfiguration.get(config.props.name)
1037
+
1038
+ if (configuredEntitlementsPath) {
1039
+ config.props.buildSettings.CODE_SIGN_ENTITLEMENTS = configuredEntitlementsPath
1040
+ continue
1041
+ }
1042
+
1043
+ if (!entitlementsPath) {
1044
+ delete config.props.buildSettings.CODE_SIGN_ENTITLEMENTS
1045
+ continue
1046
+ }
1047
+
1048
+ // A build configuration pointing at its own entitlements file keeps it, unless the config file
1049
+ // named one explicitly.
1050
+ if (!isConfiguredExplicitly && readBuildSettingString(config.props.buildSettings.CODE_SIGN_ENTITLEMENTS)) {
857
1051
  continue
858
1052
  }
859
1053
 
860
- delete config.props.buildSettings.CODE_SIGN_ENTITLEMENTS
1054
+ config.props.buildSettings.CODE_SIGN_ENTITLEMENTS = entitlementsPath
861
1055
  }
862
1056
  }
863
1057
 
@@ -886,6 +1080,17 @@ interface MainAppCodeSigningSettings {
886
1080
  provisioningProfileSpecifier?: string
887
1081
  }
888
1082
 
1083
+ interface MainAppConfigurationSettings {
1084
+ bundleIdentifier: string
1085
+ codeSigning: MainAppCodeSigningSettings
1086
+ }
1087
+
1088
+ interface MainAppSettingsByConfiguration {
1089
+ byConfigurationName: Map<string, MainAppConfigurationSettings>
1090
+ /** Used for widget build configurations the main app target does not have. */
1091
+ fallback: MainAppConfigurationSettings
1092
+ }
1093
+
889
1094
  function readBuildSettingString(value: unknown): string | undefined {
890
1095
  return typeof value === 'string' ? stripQuotes(value) : undefined
891
1096
  }