voltra 2.2.0 → 2.3.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.
Files changed (130) hide show
  1. package/build/cjs/apply/index.js +4 -1
  2. package/build/cjs/apply/index.js.map +1 -1
  3. package/build/cjs/config/normalize.js +131 -29
  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/index.js +6 -2
  16. package/build/cjs/index.js.map +1 -1
  17. package/build/cjs/platforms/android/generated.js +98 -40
  18. package/build/cjs/platforms/android/generated.js.map +1 -1
  19. package/build/cjs/platforms/android/widgetSizing.js +89 -0
  20. package/build/cjs/platforms/android/widgetSizing.js.map +1 -0
  21. package/build/cjs/platforms/ios/apply.js +21 -11
  22. package/build/cjs/platforms/ios/apply.js.map +1 -1
  23. package/build/cjs/platforms/ios/buildConfigurationValues.js +73 -0
  24. package/build/cjs/platforms/ios/buildConfigurationValues.js.map +1 -0
  25. package/build/cjs/platforms/ios/entitlements.js +34 -19
  26. package/build/cjs/platforms/ios/entitlements.js.map +1 -1
  27. package/build/cjs/platforms/ios/generated.js +79 -32
  28. package/build/cjs/platforms/ios/generated.js.map +1 -1
  29. package/build/cjs/platforms/ios/plist.js +22 -4
  30. package/build/cjs/platforms/ios/plist.js.map +1 -1
  31. package/build/cjs/platforms/ios/targetName.js.map +1 -1
  32. package/build/cjs/platforms/ios/xcodeTarget.js +166 -23
  33. package/build/cjs/platforms/ios/xcodeTarget.js.map +1 -1
  34. package/build/cjs/platforms/shared/widgetModule.js +18 -145
  35. package/build/cjs/platforms/shared/widgetModule.js.map +1 -1
  36. package/build/esm/apply/index.js +4 -1
  37. package/build/esm/apply/index.js.map +1 -1
  38. package/build/esm/config/normalize.js +131 -29
  39. package/build/esm/config/normalize.js.map +1 -1
  40. package/build/esm/config/perConfiguration.js +25 -0
  41. package/build/esm/config/perConfiguration.js.map +1 -0
  42. package/build/esm/config/serverUpdate.js +112 -0
  43. package/build/esm/config/serverUpdate.js.map +1 -0
  44. package/build/esm/config/widgetKind.js +20 -0
  45. package/build/esm/config/widgetKind.js.map +1 -0
  46. package/build/esm/dependencies/platformPackages.js +8 -0
  47. package/build/esm/dependencies/platformPackages.js.map +1 -1
  48. package/build/esm/discovery/ios.js +50 -31
  49. package/build/esm/discovery/ios.js.map +1 -1
  50. package/build/esm/index.js +1 -0
  51. package/build/esm/index.js.map +1 -1
  52. package/build/esm/platforms/android/generated.js +100 -42
  53. package/build/esm/platforms/android/generated.js.map +1 -1
  54. package/build/esm/platforms/android/widgetSizing.js +85 -0
  55. package/build/esm/platforms/android/widgetSizing.js.map +1 -0
  56. package/build/esm/platforms/ios/apply.js +21 -11
  57. package/build/esm/platforms/ios/apply.js.map +1 -1
  58. package/build/esm/platforms/ios/buildConfigurationValues.js +68 -0
  59. package/build/esm/platforms/ios/buildConfigurationValues.js.map +1 -0
  60. package/build/esm/platforms/ios/entitlements.js +34 -19
  61. package/build/esm/platforms/ios/entitlements.js.map +1 -1
  62. package/build/esm/platforms/ios/generated.js +80 -33
  63. package/build/esm/platforms/ios/generated.js.map +1 -1
  64. package/build/esm/platforms/ios/plist.js +22 -4
  65. package/build/esm/platforms/ios/plist.js.map +1 -1
  66. package/build/esm/platforms/ios/targetName.js.map +1 -1
  67. package/build/esm/platforms/ios/xcodeTarget.js +166 -23
  68. package/build/esm/platforms/ios/xcodeTarget.js.map +1 -1
  69. package/build/esm/platforms/shared/widgetModule.js +16 -107
  70. package/build/esm/platforms/shared/widgetModule.js.map +1 -1
  71. package/build/types/apply/index.d.ts.map +1 -1
  72. package/build/types/config/normalize.d.ts.map +1 -1
  73. package/build/types/config/perConfiguration.d.ts +18 -0
  74. package/build/types/config/perConfiguration.d.ts.map +1 -0
  75. package/build/types/config/serverUpdate.d.ts +80 -0
  76. package/build/types/config/serverUpdate.d.ts.map +1 -0
  77. package/build/types/config/types.d.ts +91 -30
  78. package/build/types/config/types.d.ts.map +1 -1
  79. package/build/types/config/widgetKind.d.ts +11 -0
  80. package/build/types/config/widgetKind.d.ts.map +1 -0
  81. package/build/types/dependencies/platformPackages.d.ts +1 -0
  82. package/build/types/dependencies/platformPackages.d.ts.map +1 -1
  83. package/build/types/discovery/ios.d.ts +10 -0
  84. package/build/types/discovery/ios.d.ts.map +1 -1
  85. package/build/types/index.d.ts +3 -1
  86. package/build/types/index.d.ts.map +1 -1
  87. package/build/types/platforms/android/generated.d.ts.map +1 -1
  88. package/build/types/platforms/android/widgetSizing.d.ts +50 -0
  89. package/build/types/platforms/android/widgetSizing.d.ts.map +1 -0
  90. package/build/types/platforms/ios/apply.d.ts.map +1 -1
  91. package/build/types/platforms/ios/buildConfigurationValues.d.ts +33 -0
  92. package/build/types/platforms/ios/buildConfigurationValues.d.ts.map +1 -0
  93. package/build/types/platforms/ios/entitlements.d.ts +4 -4
  94. package/build/types/platforms/ios/entitlements.d.ts.map +1 -1
  95. package/build/types/platforms/ios/generated.d.ts +14 -2
  96. package/build/types/platforms/ios/generated.d.ts.map +1 -1
  97. package/build/types/platforms/ios/plist.d.ts +3 -3
  98. package/build/types/platforms/ios/plist.d.ts.map +1 -1
  99. package/build/types/platforms/ios/podfile.d.ts +2 -2
  100. package/build/types/platforms/ios/podfile.d.ts.map +1 -1
  101. package/build/types/platforms/ios/targetName.d.ts +2 -2
  102. package/build/types/platforms/ios/targetName.d.ts.map +1 -1
  103. package/build/types/platforms/ios/xcodeTarget.d.ts +4 -2
  104. package/build/types/platforms/ios/xcodeTarget.d.ts.map +1 -1
  105. package/build/types/platforms/shared/widgetModule.d.ts +12 -3
  106. package/build/types/platforms/shared/widgetModule.d.ts.map +1 -1
  107. package/package.json +5 -5
  108. package/src/apply/index.ts +4 -1
  109. package/src/config/normalize.node.test.ts +61 -0
  110. package/src/config/normalize.ts +188 -44
  111. package/src/config/perConfiguration.ts +49 -0
  112. package/src/config/serverUpdate.ts +160 -0
  113. package/src/config/types.ts +97 -30
  114. package/src/config/widgetKind.node.test.ts +27 -0
  115. package/src/config/widgetKind.ts +27 -0
  116. package/src/dependencies/platformPackages.ts +11 -0
  117. package/src/discovery/ios.ts +104 -60
  118. package/src/index.ts +8 -0
  119. package/src/platforms/android/generated.ts +144 -47
  120. package/src/platforms/android/widgetSizing.ts +112 -0
  121. package/src/platforms/ios/apply.ts +21 -11
  122. package/src/platforms/ios/buildConfigurationValues.ts +105 -0
  123. package/src/platforms/ios/entitlements.ts +55 -31
  124. package/src/platforms/ios/generated.node.test.ts +61 -0
  125. package/src/platforms/ios/generated.ts +137 -38
  126. package/src/platforms/ios/plist.ts +36 -7
  127. package/src/platforms/ios/podfile.ts +2 -2
  128. package/src/platforms/ios/targetName.ts +2 -2
  129. package/src/platforms/ios/xcodeTarget.ts +248 -43
  130. package/src/platforms/shared/widgetModule.ts +22 -140
@@ -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
  }
@@ -1,153 +1,35 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import vm from 'node:vm'
4
- import { createRequire } from 'node:module'
1
+ import { createWidgetModuleLoader, type WidgetModuleLoader } from '@use-voltra/compiler'
5
2
 
6
- import * as babel from '@babel/core'
7
-
8
- const MODULE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '']
3
+ import type { VoltraPlatform } from '../../config/types'
9
4
 
10
5
  export interface WidgetModuleBuildInfo {
11
6
  isDev: false
12
7
  metroUrl: null
13
8
  appVersion: 'unknown'
14
- voltraVersion: '1.4.1'
15
- }
16
-
17
- export const DYNAMIC_WIDGET_BUILD_INFO: WidgetModuleBuildInfo = {
18
- isDev: false,
19
- metroUrl: null,
20
- appVersion: 'unknown',
21
- voltraVersion: '1.4.1',
9
+ voltraVersion: string
22
10
  }
23
11
 
24
- export function evaluateWidgetModuleExports(
25
- projectRoot: string,
26
- filePath: string,
27
- createError: (message: string) => Error
28
- ): unknown {
29
- const projectRequire = createProjectRequire(projectRoot)
30
- const moduleCache = new Map<string, unknown>()
31
-
32
- const customRequire = (moduleSpecifier: string, currentDir: string): unknown => {
33
- if (!isLocalModule(moduleSpecifier)) {
34
- return projectRequire(moduleSpecifier)
35
- }
36
-
37
- const resolvedModulePath = resolveModulePath(moduleSpecifier, currentDir)
38
-
39
- if (!resolvedModulePath) {
40
- throw createError(`Cannot resolve module '${moduleSpecifier}' from '${currentDir}'`)
41
- }
42
-
43
- const cachedModule = moduleCache.get(resolvedModulePath)
44
- if (cachedModule !== undefined) {
45
- return cachedModule
46
- }
47
-
48
- const transpiledCode = transpileWidgetModule(projectRoot, resolvedModulePath, projectRequire, createError)
49
- const moduleDir = path.dirname(resolvedModulePath)
50
- const moduleRecord = { exports: {} as Record<string, unknown> }
51
- moduleCache.set(resolvedModulePath, moduleRecord.exports)
52
-
53
- const context = vm.createContext({
54
- __dirname: moduleDir,
55
- __filename: resolvedModulePath,
56
- console,
57
- exports: moduleRecord.exports,
58
- module: moduleRecord,
59
- process,
60
- require: (specifier: string) => customRequire(specifier, moduleDir),
61
- })
62
-
63
- const script = new vm.Script(transpiledCode, { filename: resolvedModulePath })
64
- script.runInContext(context)
65
-
66
- moduleCache.set(resolvedModulePath, moduleRecord.exports)
67
- return moduleRecord.exports
12
+ export function createDynamicWidgetBuildInfo(voltraVersion: string): WidgetModuleBuildInfo {
13
+ return {
14
+ isDev: false,
15
+ metroUrl: null,
16
+ appVersion: 'unknown',
17
+ voltraVersion,
68
18
  }
69
-
70
- return customRequire(filePath, path.dirname(filePath))
71
19
  }
72
20
 
73
- function transpileWidgetModule(
21
+ /**
22
+ * Loader used by the apply pipeline to evaluate widget source at build time.
23
+ *
24
+ * The evaluation rules — which imports are allowed and what they resolve to — live in
25
+ * `@use-voltra/compiler` so that `voltra apply`, the Expo config plugins, and the Metro
26
+ * widget bundler all apply the same contract.
27
+ */
28
+ export function createGeneratedWidgetModuleLoader(
74
29
  projectRoot: string,
75
- filePath: string,
76
- projectRequire: NodeRequire,
77
- createError: (message: string) => Error
78
- ): string {
79
- const source = fs.readFileSync(filePath, 'utf8')
80
- const projectBabelConfigPath = resolveProjectBabelConfig(projectRoot)
81
- const result = babel.transformSync(source, {
82
- babelrc: false,
83
- configFile: projectBabelConfigPath,
84
- cwd: projectRoot,
85
- filename: filePath,
86
- presets: projectBabelConfigPath ? undefined : [resolveFallbackBabelPreset(projectRequire, createError)],
87
- })
88
-
89
- if (!result?.code) {
90
- throw createError(`Babel transpilation failed for ${filePath}`)
91
- }
92
-
93
- return result.code
94
- }
95
-
96
- function resolveProjectBabelConfig(projectRoot: string): string | undefined {
97
- const candidates = ['babel.config.js', 'babel.config.cjs', 'babel.config.mjs']
98
-
99
- for (const candidate of candidates) {
100
- const candidatePath = path.join(projectRoot, candidate)
101
- if (fs.existsSync(candidatePath)) {
102
- return candidatePath
103
- }
104
- }
105
-
106
- return undefined
107
- }
108
-
109
- function resolveFallbackBabelPreset(projectRequire: NodeRequire, createError: (message: string) => Error): string {
110
- try {
111
- return projectRequire.resolve('@react-native/babel-preset')
112
- } catch {
113
- try {
114
- return projectRequire.resolve('babel-preset-expo')
115
- } catch {
116
- throw createError(
117
- 'Could not resolve a Babel preset for widget evaluation. Add a project babel.config.js or install @react-native/babel-preset.'
118
- )
119
- }
120
- }
121
- }
122
-
123
- function createProjectRequire(projectRoot: string): NodeRequire {
124
- return createRequire(path.join(projectRoot, 'package.json'))
125
- }
126
-
127
- function isLocalModule(moduleSpecifier: string): boolean {
128
- return moduleSpecifier.startsWith('.') || moduleSpecifier.startsWith('/')
129
- }
130
-
131
- function resolveModulePath(moduleSpecifier: string, fromDir: string): string | null {
132
- const basePath = path.resolve(fromDir, moduleSpecifier)
133
-
134
- for (const extension of MODULE_EXTENSIONS) {
135
- const candidate = `${basePath}${extension}`
136
- if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
137
- return candidate
138
- }
139
- }
140
-
141
- if (!fs.existsSync(basePath) || !fs.statSync(basePath).isDirectory()) {
142
- return null
143
- }
144
-
145
- for (const extension of MODULE_EXTENSIONS) {
146
- const indexCandidate = path.join(basePath, `index${extension}`)
147
- if (fs.existsSync(indexCandidate) && fs.statSync(indexCandidate).isFile()) {
148
- return indexCandidate
149
- }
150
- }
151
-
152
- return null
30
+ platform: VoltraPlatform,
31
+ createError: (message: string) => Error,
32
+ onWarning?: (message: string) => void
33
+ ): WidgetModuleLoader {
34
+ return createWidgetModuleLoader({ projectRoot, platform, createError, onWarning })
153
35
  }