refactor(ios)!: unify widget Xcode pipeline, per-config signing, version passthrough#213
Merged
Conversation
applyXcodeChanges previously had two full code paths: a create-only one (addXCConfigurationList/addProductFile/configureTarget/addPbxGroup/ addBuildPhases) for fresh projects and an ensure one for existing targets. The fresh case is just the ensure case with nothing found, so the two paths are now one: when the target is absent a minimal native-target skeleton is created (createTargetSkeleton — targets cannot be "ensured" into existence), and everything else (configuration list, product file, group, build phases, attributes, dependency) goes through the same idempotent ensure functions in both cases. The group is still ensured before the phases, since phases resolve files through the widget-scoped group. Cleanup: - addBuildPhases deleted (only caller was the fresh path) - addXCConfigurationList/addPbxGroup/addProductFile demoted to private create-only fallbacks of their ensure siblings - configureTarget and its helpers replaced by createTargetSkeleton; target dependency and attributes now come from ensureTargetDependency/ ensureTargetAttributes on both paths - AddBuildPhasesOptions folded into EnsureBuildPhasesOptions Bug fixed along the way: the xcode lib's buildPhaseObject falls back to a project-wide comment search when the given target has no matching phase, which on the unified pipeline returned the MAIN APP's Sources/Frameworks/Resources phases for a freshly created widget target. ensureBuildPhases now looks phases up strictly within the target's own buildPhases list (findTargetPhaseByComment). Snapshot updated: content is identical to the previous snapshot modulo deterministic-UUID renumbering and pbxproj section emission order (the copy-files phase, target dependency and container proxy are now created later in the unified pipeline). Verified byte-identical after normalizing UUIDs and sorting lines.
pbxproj comments are decorative and other tools strip them, so finding a target's Sources/Frameworks/Resources phase by its comment string is fragile. ensureBuildPhases now finds a target's phase of a given type by walking target.buildPhases and checking membership in the type's object section (first match wins); the embed phase was already matched semantically via dstSubfolderSpec == 13 and now is on the creation fallback path too. Dedupe reworked accordingly: - dedupeBuildPhasesForTarget dedupes by type-section membership (keeps the first phase in buildPhases order, no comment preference) - dedupeBuildPhasesByComment replaced by dedupeEmbedPhasesForTarget, which dedupes dstSubfolderSpec == 13 copy-files phases only and leaves other copy-files phases untouched - orphan phase-object deletion behavior kept No serialized-output change: the applyXcodeChanges snapshot is byte-identical.
getMainAppTargetSettings read only the FIRST build configuration of the main app target (usually Debug), so the widget extension received Debug signing (DEVELOPMENT_TEAM/CODE_SIGN_STYLE/PROVISIONING_PROFILE_SPECIFIER) for both of its configurations — Release provisioning often differs and archive builds then failed to sign. - getMainAppTargetSettings now returns signing settings keyed by configuration name plus a first-configuration fallback - ensureXCConfigurationList applies settings per widget configuration name (Debug→Debug, Release→Release) and falls back to the first main app configuration's settings for unknown names - new fixture with-per-config-signing.pbxproj (main app Debug/Release carry different PROVISIONING_PROFILE_SPECIFIER) with tests asserting the per-config mapping on both the fresh and idempotent re-run paths The unused deploymentTarget passthrough in MainAppTargetSettings is gone: applyXcodeChanges always used the plugin's own deploymentTarget. Snapshot unchanged (fixtures a/b carry no signing settings).
configurationList.ts hardcoded CURRENT_PROJECT_VERSION="1" and MARKETING_VERSION="1.0" while the plugin's real version/buildNumber only reached Info.plist generation. App Store Connect expects the appex CFBundleShortVersionString to match the app's, so uploads with a mismatched appex version were rejected. version and buildNumber (already computed in src/index.ts and passed to withIOSWidget) now flow through configureXcodeProject props → applyXcodeChanges → ensureXCConfigurationList: MARKETING_VERSION = version, CURRENT_PROJECT_VERSION = buildNumber. The previous "1.0"/"1" values remain as fallbacks when the props are not provided (e.g. direct applyXcodeChanges callers in tests). Tests assert the values land in both Debug and Release build settings, plus the fallback. Snapshot unchanged (the snapshot test passes no version props).
withVoltraIos silently returned the config untouched when config.ios.bundleIdentifier was absent, so the whole plugin no-oped and users filed confused issues about widgets never appearing. It now throws with an actionable message pointing at setting "ios.bundleIdentifier" in app.json/app.config and re-running prebuild. BREAKING CHANGE: prebuild now fails loudly instead of silently skipping Voltra configuration when the iOS bundle identifier is not set.
ff98f0c to
c5d40b3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Stacked on #211 (
fix/xcode-pbxproj-corruption) — review only the last five commits. Merge #211 first, then retarget this PR tomain.What is this?
This PR restructures how the plugin manages the widget's Xcode target and closes several long-standing behavioral gaps. It is aimed at maintainers: the corruption fixes in #211 made the pbxproj mutation code testable, and this follow-up removes the structural duplication that made those bugs likely in the first place. It also ships three user-visible corrections: widget signing now mirrors the main app per build configuration, the widget inherits the app's version and build number, and a missing
ios.bundleIdentifierfails loudly instead of silently disabling the plugin.One commit per concern:
refactor(ios): collapse the add/ensure duality into a single ensure pipelinerefactor(ios): match build phases semantically instead of by commentfix(ios): sync widget code signing per build configurationfix(ios): thread app version/buildNumber into widget build settingsfeat(ios)!: throw whenexpo.ios.bundleIdentifieris missingHow does it work?
add*(create) andensure*(reconcile) code paths that had to be kept behaviorally identical by hand. Now the only create-only step iscreateTargetSkeleton— a target cannot be "ensured" into existence — and everything else (configuration list, product file, group, build phases, target attributes, dependency) goes through one idempotent ensure chain regardless of whether the target already existed. The collapse surfaced a latent bug: thexcodelib'sbuildPhaseObjectfalls back to a project-wide comment search and was returning the main app's Sources phase for a fresh widget target; phase lookup is now scoped to the target's ownbuildPhaseslist.dstSubfolderSpec == 13), never by comment strings, which are decorative and stripped by other tools. Deduplication follows the same rule and leaves unrelated copy-files phases untouched.getMainAppTargetSettingsnow reads every build configuration of the main app instead of only the first one, and the widget's Debug/Release configurations each receive their name-matched signing settings (CODE_SIGN_STYLE,DEVELOPMENT_TEAM,PROVISIONING_PROFILE_SPECIFIER), with a first-configuration fallback for unmatched names. Previously the Debug signing was copied into Release, which breaks manual-signing release builds with distinct provisioning profiles.expo.versionandexpo.ios.buildNumberare threaded into the widget'sMARKETING_VERSIONandCURRENT_PROJECT_VERSION(previously hardcoded"1.0"/"1", which App Store Connect flags because an appex version must match its host app). The hardcoded values remain as fallback when the app config doesn't provide them.ios.bundleIdentifierthe plugin used to return the config untouched, so prebuild "succeeded" with no widget and nothing explaining why. It now throws with a message pointing at the exact app-config key to set. Technically breaking for configs that relied on the silent no-op, hence the!.All existing fixture/consistency/idempotency tests from #211 stay untouched and green after every commit; the branch adds a per-config-signing fixture and tests, version-passthrough tests, and bundle-identifier tests (38 tests total).
Why is this useful?
1.0 (1).