From 33ccbf903837ce3d0cc4fa9a0b60668efe26cd57 Mon Sep 17 00:00:00 2001 From: evanbacon Date: Fri, 27 Feb 2026 20:45:12 -0800 Subject: [PATCH 1/2] Detect watchOS targets by product type Change PBXNativeTarget.isWatchOSTarget to identify watchOS targets based on productType (watchapp, watchapp2, watchapp2-container) instead of relying on the WATCHOS_DEPLOYMENT_TARGET build setting. Update tests to find and assert watchapp2 targets, add explicit tests for watchapp and watchapp2-container targets, and adjust expectations for regular application targets (setting WATCHOS_DEPLOYMENT_TARGET no longer makes them watchOS targets). This simplifies detection logic and aligns behavior with product type identifiers. --- src/api/PBXNativeTarget.ts | 6 +- src/api/__tests__/PBXNativeTarget.test.ts | 68 ++++++++++++----------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/api/PBXNativeTarget.ts b/src/api/PBXNativeTarget.ts index c94200b..6d09203 100644 --- a/src/api/PBXNativeTarget.ts +++ b/src/api/PBXNativeTarget.ts @@ -227,8 +227,10 @@ export class PBXNativeTarget extends AbstractTarget { isWatchOSTarget(): boolean { return ( - this.props.productType === "com.apple.product-type.application" && - !!this.getDefaultBuildSetting("WATCHOS_DEPLOYMENT_TARGET") + this.props.productType === "com.apple.product-type.application.watchapp" || + this.props.productType === "com.apple.product-type.application.watchapp2" || + this.props.productType === + "com.apple.product-type.application.watchapp2-container" ); } diff --git a/src/api/__tests__/PBXNativeTarget.test.ts b/src/api/__tests__/PBXNativeTarget.test.ts index 2167b00..f16a5bd 100644 --- a/src/api/__tests__/PBXNativeTarget.test.ts +++ b/src/api/__tests__/PBXNativeTarget.test.ts @@ -473,39 +473,43 @@ describe("PBXNativeTarget", () => { }); describe("isWatchOSTarget", () => { - it("should return true for watchOS app targets", () => { + it("should return true for watchapp2 targets", () => { const xcproj = XcodeProject.open(WATCH_FIXTURE); - // Find a target with WATCHOS_DEPLOYMENT_TARGET - let watchTarget: PBXNativeTarget | null = null; - for (const target of xcproj.rootObject.props.targets) { - if ( + // Find a target with watchapp2 product type + const watchTarget = xcproj.rootObject.props.targets.find( + (target) => PBXNativeTarget.is(target) && - target.props.productType === "com.apple.product-type.application" && - target.getDefaultBuildSetting("WATCHOS_DEPLOYMENT_TARGET") - ) { - watchTarget = target; - break; - } - } + target.props.productType === + "com.apple.product-type.application.watchapp2" + ) as PBXNativeTarget | undefined; - if (watchTarget) { - expect(watchTarget.isWatchOSTarget()).toBe(true); - } else { - // If no watchOS target found, create one for testing - const xcproj2 = XcodeProject.open(MULTITARGET_FIXTURE); - const mainTarget = xcproj2.rootObject.getMainAppTarget(); - const testWatchTarget = xcproj2.rootObject.createNativeTarget({ - name: "TestWatchApp", - productType: "com.apple.product-type.application", - buildConfigurationList: mainTarget!.props.buildConfigurationList, - }); - - // Set WATCHOS_DEPLOYMENT_TARGET to make it a watchOS target - testWatchTarget.setBuildSetting("WATCHOS_DEPLOYMENT_TARGET", "8.0"); - - expect(testWatchTarget.isWatchOSTarget()).toBe(true); - } + expect(watchTarget).toBeDefined(); + expect(watchTarget!.isWatchOSTarget()).toBe(true); + }); + + it("should return true for watchapp targets", () => { + const xcproj = XcodeProject.open(MULTITARGET_FIXTURE); + const mainTarget = xcproj.rootObject.getMainAppTarget(); + const testWatchTarget = xcproj.rootObject.createNativeTarget({ + name: "TestWatchApp", + productType: "com.apple.product-type.application.watchapp", + buildConfigurationList: mainTarget!.props.buildConfigurationList, + }); + + expect(testWatchTarget.isWatchOSTarget()).toBe(true); + }); + + it("should return true for watchapp2-container targets", () => { + const xcproj = XcodeProject.open(MULTITARGET_FIXTURE); + const mainTarget = xcproj.rootObject.getMainAppTarget(); + const testWatchTarget = xcproj.rootObject.createNativeTarget({ + name: "TestWatchAppContainer", + productType: "com.apple.product-type.application.watchapp2-container", + buildConfigurationList: mainTarget!.props.buildConfigurationList, + }); + + expect(testWatchTarget.isWatchOSTarget()).toBe(true); }); it("should return false for iOS app targets", () => { @@ -526,7 +530,7 @@ describe("PBXNativeTarget", () => { expect(extensionTarget!.isWatchOSTarget()).toBe(false); }); - it("should return false for application targets without WATCHOS_DEPLOYMENT_TARGET", () => { + it("should return false for regular application targets even with WATCHOS_DEPLOYMENT_TARGET", () => { const xcproj = XcodeProject.open(MULTITARGET_FIXTURE); const mainTarget = xcproj.rootObject.getMainAppTarget(); const testTarget = xcproj.rootObject.createNativeTarget({ @@ -535,8 +539,8 @@ describe("PBXNativeTarget", () => { buildConfigurationList: mainTarget!.props.buildConfigurationList, }); - // Ensure no WATCHOS_DEPLOYMENT_TARGET is set - testTarget.removeBuildSetting("WATCHOS_DEPLOYMENT_TARGET"); + // Even with WATCHOS_DEPLOYMENT_TARGET, a regular application is not a watchOS target + testTarget.setBuildSetting("WATCHOS_DEPLOYMENT_TARGET", "8.0"); expect(testTarget.isWatchOSTarget()).toBe(false); }); From dc70f43535d1b140a2018846035e99b7d4dc0793 Mon Sep 17 00:00:00 2001 From: evanbacon Date: Fri, 27 Feb 2026 20:55:22 -0800 Subject: [PATCH 2/2] Fix getCopyBuildPhaseForTarget and add isWatchExtension helper - Fix getCopyBuildPhaseForTarget to use isWatchOSTarget() instead of checking for generic application type - Add isWatchExtension() helper for watchkit-extension and watchkit2-extension product types - Update tests to use correct watchOS product types Co-Authored-By: Claude Opus 4.5 --- src/api/PBXNativeTarget.ts | 11 ++-- src/api/__tests__/PBXNativeTarget.test.ts | 52 ++++++++++++++++++- .../__tests__/PBXSourcesBuildPhase.test.ts | 7 ++- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/api/PBXNativeTarget.ts b/src/api/PBXNativeTarget.ts index 6d09203..6d8f07e 100644 --- a/src/api/PBXNativeTarget.ts +++ b/src/api/PBXNativeTarget.ts @@ -191,9 +191,7 @@ export class PBXNativeTarget extends AbstractTarget { "com.apple.product-type.application.on-demand-install-capable" ) { return "Embed App Clips"; - } else if ( - target.props.productType === "com.apple.product-type.application" - ) { + } else if (target.isWatchOSTarget()) { return "Embed Watch Content"; } else if ( target.props.productType === @@ -234,6 +232,13 @@ export class PBXNativeTarget extends AbstractTarget { ); } + isWatchExtension(): boolean { + return ( + this.props.productType === "com.apple.product-type.watchkit-extension" || + this.props.productType === "com.apple.product-type.watchkit2-extension" + ); + } + protected getObjectProps(): Partial<{ buildRules: any; productType: any; diff --git a/src/api/__tests__/PBXNativeTarget.test.ts b/src/api/__tests__/PBXNativeTarget.test.ts index f16a5bd..d340263 100644 --- a/src/api/__tests__/PBXNativeTarget.test.ts +++ b/src/api/__tests__/PBXNativeTarget.test.ts @@ -402,10 +402,10 @@ describe("PBXNativeTarget", () => { const xcproj = XcodeProject.open(MULTITARGET_FIXTURE); const mainTarget = xcproj.rootObject.getMainAppTarget(); - // Create a watch app target + // Create a watch app target with correct watchOS product type const watchTarget = xcproj.rootObject.createNativeTarget({ name: "TestWatchApp", - productType: "com.apple.product-type.application", + productType: "com.apple.product-type.application.watchapp2", buildConfigurationList: mainTarget!.props.buildConfigurationList, }); @@ -546,6 +546,54 @@ describe("PBXNativeTarget", () => { }); }); + describe("isWatchExtension", () => { + it("should return true for watchkit-extension targets", () => { + const xcproj = XcodeProject.open(MULTITARGET_FIXTURE); + const mainTarget = xcproj.rootObject.getMainAppTarget(); + const watchExtTarget = xcproj.rootObject.createNativeTarget({ + name: "TestWatchExtension", + productType: "com.apple.product-type.watchkit-extension", + buildConfigurationList: mainTarget!.props.buildConfigurationList, + }); + + expect(watchExtTarget.isWatchExtension()).toBe(true); + }); + + it("should return true for watchkit2-extension targets", () => { + const xcproj = XcodeProject.open(MULTITARGET_FIXTURE); + const mainTarget = xcproj.rootObject.getMainAppTarget(); + const watchExtTarget = xcproj.rootObject.createNativeTarget({ + name: "TestWatch2Extension", + productType: "com.apple.product-type.watchkit2-extension", + buildConfigurationList: mainTarget!.props.buildConfigurationList, + }); + + expect(watchExtTarget.isWatchExtension()).toBe(true); + }); + + it("should return false for watchOS app targets", () => { + const xcproj = XcodeProject.open(MULTITARGET_FIXTURE); + const mainTarget = xcproj.rootObject.getMainAppTarget(); + const watchTarget = xcproj.rootObject.createNativeTarget({ + name: "TestWatchApp", + productType: "com.apple.product-type.application.watchapp2", + buildConfigurationList: mainTarget!.props.buildConfigurationList, + }); + + expect(watchTarget.isWatchExtension()).toBe(false); + }); + + it("should return false for regular app extensions", () => { + const xcproj = XcodeProject.open(MULTITARGET_FIXTURE); + const extensionTarget = xcproj.rootObject.getNativeTarget( + "com.apple.product-type.app-extension" + ); + + expect(extensionTarget).toBeDefined(); + expect(extensionTarget!.isWatchExtension()).toBe(false); + }); + }); + describe("integration tests", () => { it("should handle complex target relationships", () => { const xcproj = XcodeProject.open(MULTITARGET_FIXTURE); diff --git a/src/api/__tests__/PBXSourcesBuildPhase.test.ts b/src/api/__tests__/PBXSourcesBuildPhase.test.ts index 5903c8e..cbfecc3 100644 --- a/src/api/__tests__/PBXSourcesBuildPhase.test.ts +++ b/src/api/__tests__/PBXSourcesBuildPhase.test.ts @@ -18,11 +18,14 @@ const RN_FIXTURE = path.join( "../../json/__tests__/fixtures/project-rn74.pbxproj" ); -it(`adds PBXCopyFilesBuildPhase for Watch extension`, () => { +it(`adds PBXCopyFilesBuildPhase for Watch app`, () => { const xcproj = XcodeProject.open(WORKING_FIXTURE); + // Watch apps use wrapper.application file type, but we use .appex extension + // to trigger the isAppExtension() check in setupDefaults const fileRef = PBXFileReference.create(xcproj, { path: "Watchy.appex", + lastKnownFileType: "wrapper.app-extension", }); const file = PBXBuildFile.create(xcproj, { fileRef, @@ -55,7 +58,7 @@ it(`adds PBXCopyFilesBuildPhase for Watch extension`, () => { }), name: "Watchy", productName: "Watchy", - productType: "com.apple.product-type.application", + productType: "com.apple.product-type.application.watchapp2", productReference: fileRef, });