From b5d667080fac90fb4d32c5fb846e7469e3e0bd1e Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Tue, 11 Aug 2026 17:19:31 +0200 Subject: [PATCH 1/2] feat(macos): run and build Mac Catalyst apps with `ns run macos` Adds macOS as a supported platform. A Mac Catalyst app is the iOS app rebuilt against the macOS SDK, so it keeps every iOS convention -- App_Resources/iOS, the iOS runtime package, each plugin's platforms/ios folder, the iOS Podfile, the iOS bundle -- and diverges only in the directory it prepares into, platforms/macos, and the SDK it builds against. That is the whole design: iOSProjectService reports iOS as the platform name so every iOS convention falls out for free, and projectRoot is the single place macOS differs. Two call sites rebuilt the platform directory from the platform name rather than reading projectRoot, which would have made a macOS build probe and delete platforms/ios; both now use projectRoot, which already resolves hostProjectPath the same way. The Mac is modelled as a device so build, deploy and LiveSync drive it through the existing pipeline. Everything it does is local: the .app is a directory on this machine, so the file system is a plain copy, install only records the built bundle, launch is `open -n`, and the log stream is `log stream` narrowed to the app and the runtime. Watch-mode prepare events are stamped with the platform the caller asked for rather than the platform data's name. run-controller pairs an event with a device by comparing the two, so for Catalyst -- iOS platform data, macOS device -- a file change recompiled but reached no device. The non-watch path already reported the requested platform; watchers and bundler processes are now keyed by it too, since stopWatchers and stopBundlerCompiler are called with it. Nothing changes for ios/android/visionos, where both strings are identical. Co-Authored-By: Claude Opus 5 --- lib/bootstrap.ts | 2 + lib/commands/build.ts | 58 +++++++ lib/commands/run.ts | 29 ++++ lib/common/bootstrap.ts | 4 + lib/common/definitions/mobile.d.ts | 10 ++ .../mobile/device-platforms-constants.ts | 6 + .../mac/mac-catalyst-application-manager.ts | 145 ++++++++++++++++++ lib/common/mobile/mac/mac-catalyst-device.ts | 100 ++++++++++++ .../mobile/mac/mac-catalyst-file-system.ts | 101 ++++++++++++ .../mobile/mobile-core/devices-service.ts | 3 + .../mobile-core/ios-device-discovery.ts | 2 + .../mobile-core/mac-catalyst-discovery.ts | 39 +++++ lib/common/mobile/mobile-helper.ts | 17 +- lib/constants.ts | 4 +- lib/controllers/platform-controller.ts | 13 +- lib/controllers/prepare-controller.ts | 47 +++--- lib/definitions/ios.d.ts | 10 ++ lib/definitions/project.d.ts | 3 + lib/device-path-provider.ts | 15 ++ lib/project-data.ts | 9 ++ .../bundler/bundler-compiler-service.ts | 30 ++-- lib/services/ios-project-service.ts | 42 ++++- lib/services/ios/xcodebuild-args-service.ts | 71 +++++++++ lib/services/ios/xcodebuild-service.ts | 16 ++ lib/services/platform/add-platform-service.ts | 20 +-- lib/services/platforms-data-service.ts | 2 + lib/services/project-data-service.ts | 4 + 27 files changed, 740 insertions(+), 62 deletions(-) create mode 100644 lib/common/mobile/mac/mac-catalyst-application-manager.ts create mode 100644 lib/common/mobile/mac/mac-catalyst-device.ts create mode 100644 lib/common/mobile/mac/mac-catalyst-file-system.ts create mode 100644 lib/common/mobile/mobile-core/mac-catalyst-discovery.ts diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 7c88a6d811..8a232e9dd9 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -183,6 +183,7 @@ injector.requireCommand("run|ios", "./commands/run"); injector.requireCommand("run|android", "./commands/run"); injector.requireCommand("run|vision", "./commands/run"); injector.requireCommand("run|visionos", "./commands/run"); +injector.requireCommand("run|macos", "./commands/run"); injector.requireCommand("typings", "./commands/typings"); injector.requireCommand("preview", "./commands/preview"); @@ -198,6 +199,7 @@ injector.requireCommand("build|ios", "./commands/build"); injector.requireCommand("build|android", "./commands/build"); injector.requireCommand("build|vision", "./commands/build"); injector.requireCommand("build|visionos", "./commands/build"); +injector.requireCommand("build|macos", "./commands/build"); injector.requireCommand("deploy", "./commands/deploy"); injector.requireCommand("embed", "./commands/embedding/embed"); diff --git a/lib/commands/build.ts b/lib/commands/build.ts index 7216e8a3fc..fab616d06a 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -278,3 +278,61 @@ export class BuildVisionOsCommand extends BuildIosCommand implements ICommand { injector.registerCommand("build|vision", BuildVisionOsCommand); injector.registerCommand("build|visionos", BuildVisionOsCommand); + +/** + * Builds the iOS target against the macOS SDK as a Mac Catalyst app. + */ +export class BuildMacOsCommand extends BuildIosCommand implements ICommand { + constructor( + protected $options: IOptions, + $errors: IErrors, + $projectData: IProjectData, + $platformsDataService: IPlatformsDataService, + $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + $buildController: IBuildController, + $platformValidationService: IPlatformValidationService, + $logger: ILogger, + $buildDataService: IBuildDataService, + protected $migrateController: IMigrateController, + ) { + super( + $options, + $errors, + $projectData, + $platformsDataService, + $devicePlatformsConstants, + $buildController, + $platformValidationService, + $logger, + $buildDataService, + $migrateController, + ); + } + + public async execute(args: string[]): Promise { + await this.executeCore([ + this.$devicePlatformsConstants.macOS.toLowerCase(), + ]); + } + + public async canExecute(args: string[]): Promise { + const platform = this.$devicePlatformsConstants.macOS; + if (!this.$options.force) { + await this.$migrateController.validate({ + projectDir: this.$projectData.projectDir, + platforms: [platform], + }); + } + + super.validatePlatform(platform); + + let canExecute = await super.canExecuteCommandBase(platform); + if (canExecute) { + canExecute = await super.validateArgs(args, platform); + } + + return canExecute; + } +} + +injector.registerCommand("build|macos", BuildMacOsCommand); diff --git a/lib/commands/run.ts b/lib/commands/run.ts index 8b7e789c1b..e5495277bf 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -223,3 +223,32 @@ export class RunVisionOSCommand extends RunIosCommand { injector.registerCommand("run|vision", RunVisionOSCommand); injector.registerCommand("run|visionos", RunVisionOSCommand); + +/** + * Runs the Mac Catalyst build of the app on this machine. + */ +export class RunMacOSCommand extends RunIosCommand { + public get platform(): string { + return this.$devicePlatformsConstants.macOS; + } + + constructor( + protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + protected $errors: IErrors, + protected $injector: IInjector, + protected $options: IOptions, + protected $platformValidationService: IPlatformValidationService, + protected $projectDataService: IProjectDataService, + ) { + super( + $devicePlatformsConstants, + $errors, + $injector, + $options, + $platformValidationService, + $projectDataService, + ); + } +} + +injector.registerCommand("run|macos", RunMacOSCommand); diff --git a/lib/common/bootstrap.ts b/lib/common/bootstrap.ts index 9905f452b9..9f17c625c2 100644 --- a/lib/common/bootstrap.ts +++ b/lib/common/bootstrap.ts @@ -82,6 +82,10 @@ injector.require( "iOSSimulatorDiscovery", "./mobile/mobile-core/ios-simulator-discovery" ); +injector.require( + "macCatalystDeviceDiscovery", + "./mobile/mobile-core/mac-catalyst-discovery" +); injector.require( "androidDeviceDiscovery", "./mobile/mobile-core/android-device-discovery" diff --git a/lib/common/definitions/mobile.d.ts b/lib/common/definitions/mobile.d.ts index e5db0a23c1..009e6e7073 100644 --- a/lib/common/definitions/mobile.d.ts +++ b/lib/common/definitions/mobile.d.ts @@ -138,6 +138,13 @@ declare global { destroyAllSockets(): Promise; } + interface IMacCatalystDevice extends IDevice { + /** + * Absolute path of the built .app bundle this device runs and syncs into. + */ + applicationBundlePath: string; + } + interface IAndroidDevice extends IDevice { adb: Mobile.IDeviceAndroidDebugBridge; init(): Promise; @@ -1204,6 +1211,7 @@ declare global { isAndroidPlatform(platform: string): boolean; isiOSPlatform(platform: string): boolean; isvisionOSPlatform(platform: string): boolean; + ismacOSPlatform(platform: string): boolean; isApplePlatform(platform: string): boolean; normalizePlatformName(platform: string): string; validatePlatformName(platform: string): string; @@ -1248,10 +1256,12 @@ declare global { iOS: string; Android: string; visionOS: string; + macOS: string; isiOS(value: string): boolean; isAndroid(value: string): boolean; isvisionOS(value: string): boolean; + ismacOS(value: string): boolean; } interface IDeviceApplication { diff --git a/lib/common/mobile/device-platforms-constants.ts b/lib/common/mobile/device-platforms-constants.ts index a02eb88ffe..5951242343 100644 --- a/lib/common/mobile/device-platforms-constants.ts +++ b/lib/common/mobile/device-platforms-constants.ts @@ -6,6 +6,8 @@ export class DevicePlatformsConstants public iOS = "iOS"; public Android = "Android"; public visionOS = "visionOS"; + // Not a runtime of its own: iOS rebuilt against the macOS SDK. + public macOS = "macOS"; public isiOS(value: string) { return value.toLowerCase() === this.iOS.toLowerCase(); @@ -18,5 +20,9 @@ export class DevicePlatformsConstants public isvisionOS(value: string) { return value.toLowerCase() === this.visionOS.toLowerCase(); } + + public ismacOS(value: string) { + return value.toLowerCase() === this.macOS.toLowerCase(); + } } injector.register("devicePlatformsConstants", DevicePlatformsConstants); diff --git a/lib/common/mobile/mac/mac-catalyst-application-manager.ts b/lib/common/mobile/mac/mac-catalyst-application-manager.ts new file mode 100644 index 0000000000..ba6e3e0e15 --- /dev/null +++ b/lib/common/mobile/mac/mac-catalyst-application-manager.ts @@ -0,0 +1,145 @@ +import { ChildProcess } from "child_process"; +import * as path from "path"; +import { ApplicationManagerBase } from "../application-manager-base"; +import { hook } from "../../helpers"; +import { cache } from "../../decorators"; +import { IOS_LOG_PREDICATE } from "../../constants"; +import { + IChildProcess, + IDictionary, + IFileSystem, + IHooksService, +} from "../../declarations"; +import { IOptions } from "../../../declarations"; + +export class MacCatalystApplicationManager extends ApplicationManagerBase { + private logProcess: ChildProcess = null; + + constructor( + private device: Mobile.IMacCatalystDevice, + private $childProcess: IChildProcess, + private $fs: IFileSystem, + private $options: IOptions, + protected $deviceLogProvider: Mobile.IDeviceLogProvider, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + $logger: ILogger, + $hooksService: IHooksService, + ) { + super($logger, $hooksService, $deviceLogProvider); + } + + public async getInstalledApplications(): Promise { + // Installed only means the build produced the bundle in place. + return this.$fs.exists(this.device.applicationBundlePath) + ? [this.device.deviceInfo.identifier] + : []; + } + + @hook("install") + public async installApplication(packageFilePath: string): Promise { + // No install step: just record where the build put the bundle. + this.device.applicationBundlePath = packageFilePath; + } + + public async uninstallApplication(appIdentifier: string): Promise { + await this.stopApplication({ + appId: appIdentifier, + projectName: null, + projectDir: null, + }); + } + + public async startApplication( + appData: Mobile.IStartApplicationData, + ): Promise { + await this.setDeviceLogData(appData); + + // -n forces a fresh instance instead of activating the running copy. + await this.$childProcess.spawnFromEvent( + "open", + ["-n", this.device.applicationBundlePath], + "close", + ); + } + + public async stopApplication( + appData: Mobile.IApplicationData, + ): Promise { + try { + // Anchored so it never matches our own log stream process. + await this.$childProcess.spawnFromEvent( + "pkill", + ["-f", `^${this.getExecutablePath()}$`], + "close", + ); + } catch (err) { + // pkill exits non-zero when no process matched. + this.$logger.trace( + `Nothing to stop for ${appData.appId}. More info: ${err.message}`, + ); + } + } + + public async getDebuggableApps(): Promise< + Mobile.IDeviceApplicationInformation[] + > { + return []; + } + + public async getDebuggableAppViews( + appIdentifiers: string[], + ): Promise> { + return null; + } + + private getExecutablePath(): string { + return path.join( + this.device.applicationBundlePath, + "Contents", + "MacOS", + path.basename(this.device.applicationBundlePath, ".app"), + ); + } + + private async setDeviceLogData( + appData: Mobile.IApplicationData, + ): Promise { + this.$deviceLogProvider.setProjectNameForDevice( + this.device.deviceInfo.identifier, + appData.projectName, + ); + this.$deviceLogProvider.setProjectDirForDevice( + this.device.deviceInfo.identifier, + appData.projectDir, + ); + + if (!this.$options.justlaunch) { + this.startDeviceLog(); + } + } + + @cache() + private startDeviceLog(): void { + // Narrowed to this app and the runtime, else system noise floods. + this.logProcess = this.$childProcess.spawn("/usr/bin/log", [ + "stream", + "--style", + "compact", + "--level", + "debug", + "--predicate", + `processImagePath == "${this.getExecutablePath()}" AND ${IOS_LOG_PREDICATE}`, + ]); + + const action = (data: Buffer | string) => { + this.$deviceLogProvider.logData( + data.toString(), + this.$devicePlatformsConstants.macOS, + this.device.deviceInfo.identifier, + ); + }; + + this.logProcess.stdout?.on("data", action); + this.logProcess.stderr?.on("data", action); + } +} diff --git a/lib/common/mobile/mac/mac-catalyst-device.ts b/lib/common/mobile/mac/mac-catalyst-device.ts new file mode 100644 index 0000000000..102a41e8f6 --- /dev/null +++ b/lib/common/mobile/mac/mac-catalyst-device.ts @@ -0,0 +1,100 @@ +import * as os from "os"; +import * as path from "path"; +import { MacCatalystApplicationManager } from "./mac-catalyst-application-manager"; +import { MacCatalystFileSystem } from "./mac-catalyst-file-system"; +import * as constants from "../../constants"; +import { DeviceConnectionType } from "../../../constants"; +import { IInjector } from "../../definitions/yok"; +import { IOptions } from "../../../declarations"; +import { IBuildDataService } from "../../../definitions/build"; +import { IPlatformsDataService } from "../../../definitions/platform"; +import { IProjectDataService } from "../../../definitions/project"; + +export const MAC_CATALYST_DEVICE_IDENTIFIER = "mac-catalyst"; + +/** + * The Mac exposed as a device so build, deploy and LiveSync drive a Catalyst app. + */ +export class MacCatalystDevice implements Mobile.IMacCatalystDevice { + public applicationManager: Mobile.IDeviceApplicationManager; + public fileSystem: Mobile.IDeviceFileSystem; + public deviceInfo: Mobile.IDeviceInfo; + + private _applicationBundlePath: string = null; + + constructor( + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $injector: IInjector, + private $options: IOptions, + private $buildDataService: IBuildDataService, + private $platformsDataService: IPlatformsDataService, + private $projectDataService: IProjectDataService, + ) { + this.applicationManager = this.$injector.resolve( + MacCatalystApplicationManager, + { device: this }, + ); + this.fileSystem = this.$injector.resolve(MacCatalystFileSystem); + this.deviceInfo = { + imageIdentifier: MAC_CATALYST_DEVICE_IDENTIFIER, + identifier: MAC_CATALYST_DEVICE_IDENTIFIER, + displayName: os.hostname(), + model: "Mac", + version: os.release(), + vendor: "Apple", + platform: this.$devicePlatformsConstants.macOS, + status: constants.CONNECTED_STATUS, + errorHelp: null, + isTablet: false, + type: constants.DeviceTypes.Device, + connectionTypes: [DeviceConnectionType.Local], + }; + } + + /** + * Path of the built .app, falling back to where the build would put it. + */ + public get applicationBundlePath(): string { + if (!this._applicationBundlePath) { + this._applicationBundlePath = this.getBuiltApplicationBundlePath(); + } + + return this._applicationBundlePath; + } + + public set applicationBundlePath(bundlePath: string) { + this._applicationBundlePath = bundlePath; + } + + public get isEmulator(): boolean { + return false; + } + + public get isOnlyWiFiConnected(): boolean { + return false; + } + + public async openDeviceLogStream(): Promise { + // Nothing to attach to until the application manager launches the app. + return; + } + + private getBuiltApplicationBundlePath(): string { + const projectData = this.$projectDataService.getProjectData(); + const platform = this.$devicePlatformsConstants.macOS; + const platformData = this.$platformsDataService.getPlatformData( + platform.toLowerCase(), + projectData, + ); + const buildData = this.$buildDataService.getBuildData( + projectData.projectDir, + platform, + this.$options.argv, + ); + + return path.join( + platformData.getBuildOutputPath(buildData), + `${projectData.projectName}.app`, + ); + } +} diff --git a/lib/common/mobile/mac/mac-catalyst-file-system.ts b/lib/common/mobile/mac/mac-catalyst-file-system.ts new file mode 100644 index 0000000000..d33af5817a --- /dev/null +++ b/lib/common/mobile/mac/mac-catalyst-file-system.ts @@ -0,0 +1,101 @@ +import * as path from "path"; +import * as shelljs from "shelljs"; +import * as _ from "lodash"; +import { IFileSystem, IStringDictionary } from "../../declarations"; + +/** + * Local file operations on the Mac Catalyst app bundle, which is a plain directory. + */ +export class MacCatalystFileSystem implements Mobile.IDeviceFileSystem { + constructor( + private $fs: IFileSystem, + private $logger: ILogger, + ) {} + + public async listFiles(devicePath: string): Promise { + return this.$fs.readDirectory(devicePath); + } + + public async getFile( + deviceFilePath: string, + appIdentifier: string, + outputFilePath?: string, + ): Promise { + if (outputFilePath) { + shelljs.cp("-f", deviceFilePath, outputFilePath); + } + } + + public async getFileContent( + deviceFilePath: string, + appIdentifier: string, + ): Promise { + return this.$fs.readText(deviceFilePath); + } + + public async putFile( + localFilePath: string, + deviceFilePath: string, + appIdentifier: string, + ): Promise { + shelljs.cp("-f", localFilePath, deviceFilePath); + } + + public async deleteFile( + deviceFilePath: string, + appIdentifier: string, + ): Promise { + shelljs.rm("-rf", deviceFilePath); + } + + public async transferFiles( + deviceAppData: Mobile.IDeviceAppData, + localToDevicePaths: Mobile.ILocalToDevicePathData[], + ): Promise { + await Promise.all( + _.map(localToDevicePaths, (localToDevicePathData) => + this.transferFile( + localToDevicePathData.getLocalPath(), + localToDevicePathData.getDevicePath(), + ), + ), + ); + return localToDevicePaths; + } + + public async transferDirectory( + deviceAppData: Mobile.IDeviceAppData, + localToDevicePaths: Mobile.ILocalToDevicePathData[], + projectFilesPath: string, + ): Promise { + const destinationPath = await deviceAppData.getDeviceProjectRootPath(); + this.$logger.trace( + `Transferring from ${projectFilesPath} to ${destinationPath}`, + ); + this.$fs.ensureDirectoryExists(destinationPath); + shelljs.cp("-Rf", path.join(projectFilesPath, "*"), destinationPath); + return localToDevicePaths; + } + + public async transferFile( + localFilePath: string, + deviceFilePath: string, + ): Promise { + this.$logger.trace( + `Transferring from ${localFilePath} to ${deviceFilePath}`, + ); + if (this.$fs.getFsStats(localFilePath).isDirectory()) { + this.$fs.ensureDirectoryExists(deviceFilePath); + } else { + this.$fs.ensureDirectoryExists(path.dirname(deviceFilePath)); + shelljs.cp("-f", localFilePath, deviceFilePath); + } + } + + public updateHashesOnDevice( + hashes: IStringDictionary, + appIdentifier: string, + ): Promise { + return; + } +} diff --git a/lib/common/mobile/mobile-core/devices-service.ts b/lib/common/mobile/mobile-core/devices-service.ts index 15bf11787f..6b584fa158 100644 --- a/lib/common/mobile/mobile-core/devices-service.ts +++ b/lib/common/mobile/mobile-core/devices-service.ts @@ -56,6 +56,7 @@ export class DevicesService private $emulatorHelper: Mobile.IEmulatorHelper, private $prompter: IPrompter, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $macCatalystDeviceDiscovery: Mobile.IDeviceDiscovery, ) { super(); this.attachToKnownDeviceDiscoveryEvents(); @@ -64,6 +65,7 @@ export class DevicesService this.$iOSDeviceDiscovery, this.$androidDeviceDiscovery, this.$iOSSimulatorDiscovery, + this.$macCatalystDeviceDiscovery, ]; } @@ -324,6 +326,7 @@ export class DevicesService this.$iOSSimulatorDiscovery, this.$iOSDeviceDiscovery, this.$androidDeviceDiscovery, + this.$macCatalystDeviceDiscovery, ].forEach(this.attachToDeviceDiscoveryEvents.bind(this)); } diff --git a/lib/common/mobile/mobile-core/ios-device-discovery.ts b/lib/common/mobile/mobile-core/ios-device-discovery.ts index d7b5a210e9..8ff1fef7bd 100644 --- a/lib/common/mobile/mobile-core/ios-device-discovery.ts +++ b/lib/common/mobile/mobile-core/ios-device-discovery.ts @@ -23,6 +23,8 @@ export class IOSDeviceDiscovery extends DeviceDiscovery { options && options.platform && (!this.$mobileHelper.isApplePlatform(options.platform) || + // macOS runs on this machine, not over usbmux. + this.$mobileHelper.ismacOSPlatform(options.platform) || options.emulator) ) { return; diff --git a/lib/common/mobile/mobile-core/mac-catalyst-discovery.ts b/lib/common/mobile/mobile-core/mac-catalyst-discovery.ts new file mode 100644 index 0000000000..5dc10e7f96 --- /dev/null +++ b/lib/common/mobile/mobile-core/mac-catalyst-discovery.ts @@ -0,0 +1,39 @@ +import { DeviceDiscovery } from "./device-discovery"; +import { MacCatalystDevice } from "../mac/mac-catalyst-device"; +import { IInjector } from "../../definitions/yok"; +import { IHostInfo } from "../../declarations"; +import { injector } from "../../yok"; + +export class MacCatalystDeviceDiscovery extends DeviceDiscovery { + private isDeviceAdded = false; + + constructor( + private $injector: IInjector, + private $hostInfo: IHostInfo, + private $mobileHelper: Mobile.IMobileHelper, + ) { + super(); + } + + public async startLookingForDevices( + options?: Mobile.IDeviceLookingOptions, + ): Promise { + // Only one Mac to run on, and it is this machine. + if (!this.$hostInfo.isDarwin || this.isDeviceAdded) { + return; + } + + if ( + !options || + !options.platform || + !this.$mobileHelper.ismacOSPlatform(options.platform) + ) { + return; + } + + this.addDevice(this.$injector.resolve(MacCatalystDevice)); + this.isDeviceAdded = true; + } +} + +injector.register("macCatalystDeviceDiscovery", MacCatalystDeviceDiscovery); diff --git a/lib/common/mobile/mobile-helper.ts b/lib/common/mobile/mobile-helper.ts index 666f2ffae9..c32e7ff64e 100644 --- a/lib/common/mobile/mobile-helper.ts +++ b/lib/common/mobile/mobile-helper.ts @@ -21,6 +21,7 @@ export class MobileHelper implements Mobile.IMobileHelper { this.$devicePlatformsConstants.iOS, this.$devicePlatformsConstants.Android, this.$devicePlatformsConstants.visionOS, + this.$devicePlatformsConstants.macOS, ]; } @@ -48,8 +49,20 @@ export class MobileHelper implements Mobile.IMobileHelper { ); } + public ismacOSPlatform(platform: string): boolean { + return !!( + platform && + this.$devicePlatformsConstants.macOS.toLowerCase() === + platform.toLowerCase() + ); + } + public isApplePlatform(platform: string): boolean { - return this.isiOSPlatform(platform) || this.isvisionOSPlatform(platform); + return ( + this.isiOSPlatform(platform) || + this.isvisionOSPlatform(platform) || + this.ismacOSPlatform(platform) + ); } public normalizePlatformName(platform: string): string { @@ -59,6 +72,8 @@ export class MobileHelper implements Mobile.IMobileHelper { return "iOS"; } else if (this.isvisionOSPlatform(platform)) { return "visionOS"; + } else if (this.ismacOSPlatform(platform)) { + return "macOS"; } return undefined; diff --git a/lib/constants.ts b/lib/constants.ts index 1c50e69870..35821fbdab 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -360,12 +360,14 @@ export const enum PlatformTypes { ios = "ios", android = "android", visionos = "visionos", + macos = "macos", } export type SupportedPlatform = | PlatformTypes.ios | PlatformTypes.android - | PlatformTypes.visionos; + | PlatformTypes.visionos + | PlatformTypes.macos; export const PODFILE_NAME = "Podfile"; diff --git a/lib/controllers/platform-controller.ts b/lib/controllers/platform-controller.ts index 6e572bb149..8d866bde6b 100644 --- a/lib/controllers/platform-controller.ts +++ b/lib/controllers/platform-controller.ts @@ -188,10 +188,10 @@ export class PlatformController implements IPlatformController { projectData: IProjectData, nativePrepare: INativePrepare ): boolean { - const platformName = platformData.platformNameLowerCase; - const hasPlatformDirectory = this.$fs.exists( - path.join(projectData.platformsDir, platformName) - ); + // Mac Catalyst reports iOS but prepares into platforms/macos. + const platformDirectory = platformData.projectRoot; + const platformName = path.basename(platformDirectory); + const hasPlatformDirectory = this.$fs.exists(platformDirectory); const shouldAddNativePlatform = !nativePrepare || !nativePrepare.skipNativePrepare; @@ -206,9 +206,8 @@ export class PlatformController implements IPlatformController { (shouldAddNativePlatform && requiresNativePlatformAdd); if (hasPlatformDirectory && !shouldAddPlatform) { - const platformDirectoryItemCount = this.$fs.readDirectory( - path.join(projectData.platformsDir, platformName) - ).length; + const platformDirectoryItemCount = + this.$fs.readDirectory(platformDirectory).length; // 2 is a magic number to approximate a valid platform folder // any valid platform should contain at least 2 files/folders diff --git a/lib/controllers/prepare-controller.ts b/lib/controllers/prepare-controller.ts index 5ef34172d5..d47d41d692 100644 --- a/lib/controllers/prepare-controller.ts +++ b/lib/controllers/prepare-controller.ts @@ -97,6 +97,11 @@ export class PrepareController extends EventEmitter { return this.prepareCore(prepareData, projectData); } + // Catalyst reports iOS in platform data, but events must say macos. + private getRequestedPlatform(prepareData: IPrepareData): string { + return prepareData.platform.toLowerCase(); + } + public async stopWatchers( projectDir: string, platform: string, @@ -220,18 +225,14 @@ export class PrepareController extends EventEmitter { projectData: IProjectData, prepareData: IPrepareData, ): Promise { + const requestedPlatform = this.getRequestedPlatform(prepareData); + if (!this.watchersData[projectData.projectDir]) { this.watchersData[projectData.projectDir] = {}; } - if ( - !this.watchersData[projectData.projectDir][ - platformData.platformNameLowerCase - ] - ) { - this.watchersData[projectData.projectDir][ - platformData.platformNameLowerCase - ] = { + if (!this.watchersData[projectData.projectDir][requestedPlatform]) { + this.watchersData[projectData.projectDir][requestedPlatform] = { nativeFilesWatcher: null, hasWebpackCompilerProcess: false, prepareArguments: { @@ -253,7 +254,7 @@ export class PrepareController extends EventEmitter { prepareData, ); // -> start watcher + initial prepare const result = { - platform: platformData.platformNameLowerCase, + platform: requestedPlatform, hasNativeChanges, }; @@ -274,7 +275,7 @@ export class PrepareController extends EventEmitter { hasOnlyHotUpdateFiles: false, hasNativeChanges: result.hasNativeChanges, hmrData: null, - platform: platformData.platformNameLowerCase, + platform: requestedPlatform, }); } @@ -286,15 +287,14 @@ export class PrepareController extends EventEmitter { projectData: IProjectData, prepareData: IPrepareData, ): Promise { + const requestedPlatform = this.getRequestedPlatform(prepareData); + if ( - !this.watchersData[projectData.projectDir][ - platformData.platformNameLowerCase - ].hasWebpackCompilerProcess + !this.watchersData[projectData.projectDir][requestedPlatform] + .hasWebpackCompilerProcess ) { const handler = (data: any) => { - if ( - data.platform.toLowerCase() === platformData.platformNameLowerCase - ) { + if (data.platform.toLowerCase() === requestedPlatform) { if (this.isFileWatcherPaused()) return; this.emitPrepareEvent({ ...data, hasNativeChanges: false }); } @@ -307,7 +307,7 @@ export class PrepareController extends EventEmitter { ); this.watchersData[projectData.projectDir][ - platformData.platformNameLowerCase + requestedPlatform ].hasWebpackCompilerProcess = true; await this.$bundlerCompilerService.compileWithWatch( platformData, @@ -329,6 +329,7 @@ export class PrepareController extends EventEmitter { newNativeWatchStarted = await this.startNativeWatcher( platformData, projectData, + prepareData, ); } @@ -347,11 +348,13 @@ export class PrepareController extends EventEmitter { private async startNativeWatcher( platformData: IPlatformData, projectData: IProjectData, + prepareData: IPrepareData, ): Promise { + const requestedPlatform = this.getRequestedPlatform(prepareData); + if ( - this.watchersData[projectData.projectDir][ - platformData.platformNameLowerCase - ].nativeFilesWatcher + this.watchersData[projectData.projectDir][requestedPlatform] + .nativeFilesWatcher ) { return false; } @@ -383,14 +386,14 @@ export class PrepareController extends EventEmitter { hasOnlyHotUpdateFiles: false, hmrData: null, hasNativeChanges: true, - platform: platformData.platformNameLowerCase, + platform: requestedPlatform, }); } }, ); this.watchersData[projectData.projectDir][ - platformData.platformNameLowerCase + requestedPlatform ].nativeFilesWatcher = watcher; return true; diff --git a/lib/definitions/ios.d.ts b/lib/definitions/ios.d.ts index 60c3430bbb..7940fc7e33 100644 --- a/lib/definitions/ios.d.ts +++ b/lib/definitions/ios.d.ts @@ -39,6 +39,11 @@ declare global { projectData: IProjectData, buildConfig: IBuildConfig, ): Promise; + buildForCatalyst( + platformData: IPlatformData, + projectData: IProjectData, + buildConfig: IBuildConfig, + ): Promise; } interface IosSPMPackageBase { @@ -110,6 +115,11 @@ declare global { projectData: IProjectData, buildConfig: IBuildConfig, ): Promise; + getBuildForCatalystArgs( + platformData: IPlatformData, + projectData: IProjectData, + buildConfig: IBuildConfig, + ): string[]; getXcodeProjectArgs( platformData: IPlatformData, projectData: IProjectData, diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index 4dd9a85a6a..52eaa0d865 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -138,6 +138,8 @@ interface INsConfigIOS extends INsConfigPlaform { interface INSConfigVisionOS extends INsConfigIOS {} +interface INSConfigMacOS extends INsConfigIOS {} + interface INsConfigAndroid extends INsConfigPlaform { v8Flags?: string; @@ -197,6 +199,7 @@ interface INsConfig { ios?: INsConfigIOS; android?: INsConfigAndroid; visionos?: INSConfigVisionOS; + macos?: INSConfigMacOS; ignoredNativeDependencies?: string[]; hooks?: INsConfigHooks[]; projectName?: string; diff --git a/lib/device-path-provider.ts b/lib/device-path-provider.ts index 6995c41b31..2f2ba34d4c 100644 --- a/lib/device-path-provider.ts +++ b/lib/device-path-provider.ts @@ -17,6 +17,21 @@ export class DevicePathProvider implements IDevicePathProvider { options: IDeviceProjectRootOptions ): Promise { let projectRoot = ""; + if (this.$mobileHelper.ismacOSPlatform(device.deviceInfo.platform)) { + projectRoot = (device).applicationBundlePath; + if (!projectRoot) { + this.$errors.fail("Unable to get application path on device."); + } + + // Catalyst keeps its payload under Contents/Resources, not the bundle root. + projectRoot = path.join(projectRoot, "Contents", "Resources"); + if (!options.getDirname) { + projectRoot = path.join(projectRoot, APP_FOLDER_NAME); + } + + return projectRoot; + } + if (this.$mobileHelper.isApplePlatform(device.deviceInfo.platform)) { projectRoot = device.isEmulator ? await this.$iOSSimResolver.iOSSim.getApplicationPath( diff --git a/lib/project-data.ts b/lib/project-data.ts index 277dbf32d1..37f9e9e6ef 100644 --- a/lib/project-data.ts +++ b/lib/project-data.ts @@ -326,6 +326,7 @@ export class ProjectData implements IProjectData { ios: "", android: "", visionos: "", + macos: "", }; } @@ -333,6 +334,8 @@ export class ProjectData implements IProjectData { ios: config.id, android: config.id, visionos: config.id, + // Mac Catalyst ships under the iOS bundle identifier by default. + macos: config.id, }; if (config.ios && config.ios.id) { @@ -344,6 +347,12 @@ export class ProjectData implements IProjectData { if (config.visionos && config.visionos.id) { identifier.visionos = config.visionos.id; } + if (config.ios && config.ios.id) { + identifier.macos = config.ios.id; + } + if (config.macos && config.macos.id) { + identifier.macos = config.macos.id; + } return identifier; } diff --git a/lib/services/bundler/bundler-compiler-service.ts b/lib/services/bundler/bundler-compiler-service.ts index 0a4bd8800e..710ca1f2b3 100644 --- a/lib/services/bundler/bundler-compiler-service.ts +++ b/lib/services/bundler/bundler-compiler-service.ts @@ -109,7 +109,7 @@ export class BundlerCompilerService prepareData: IPrepareData, ): Promise { return new Promise(async (resolve, reject) => { - if (this.bundlerProcesses[platformData.platformNameLowerCase]) { + if (this.bundlerProcesses[prepareData.platform.toLowerCase()]) { resolve(void 0); return; } @@ -236,7 +236,8 @@ export class BundlerCompilerService hash: (message as IBundlerEmitMessage).hash || "", fallbackFiles: [] as string[], }, - platform: platformData.platformNameLowerCase, + // Requested platform; Catalyst reports iOS in platform data. + platform: prepareData.platform.toLowerCase(), }; this.$logger.info( @@ -344,7 +345,8 @@ export class BundlerCompilerService hash: result.hash, fallbackFiles, }, - platform: platformData.platformNameLowerCase, + // Requested platform; Catalyst reports iOS in platform data. + platform: prepareData.platform.toLowerCase(), }; this.$logger.trace( @@ -367,7 +369,7 @@ export class BundlerCompilerService this.$logger.trace( `Unable to start ${projectData.bundler} process in watch mode. Error is: ${err}`, ); - delete this.bundlerProcesses[platformData.platformNameLowerCase]; + delete this.bundlerProcesses[prepareData.platform.toLowerCase()]; reject(err); }); @@ -384,7 +386,7 @@ export class BundlerCompilerService `Executing ${projectData.bundler} failed with exit code ${exitCode}.`, ); error.code = exitCode; - delete this.bundlerProcesses[platformData.platformNameLowerCase]; + delete this.bundlerProcesses[prepareData.platform.toLowerCase()]; reject(error); }); } catch (err) { @@ -399,7 +401,7 @@ export class BundlerCompilerService prepareData: IPrepareData, ): Promise { return new Promise(async (resolve, reject) => { - if (this.bundlerProcesses[platformData.platformNameLowerCase]) { + if (this.bundlerProcesses[prepareData.platform.toLowerCase()]) { resolve(); return; } @@ -415,7 +417,7 @@ export class BundlerCompilerService this.$logger.trace( `Unable to start ${projectData.bundler} process in non-watch mode. Error is: ${err}`, ); - delete this.bundlerProcesses[platformData.platformNameLowerCase]; + delete this.bundlerProcesses[prepareData.platform.toLowerCase()]; reject(err); }); @@ -426,7 +428,7 @@ export class BundlerCompilerService childProcess.pid.toString(), ); - delete this.bundlerProcesses[platformData.platformNameLowerCase]; + delete this.bundlerProcesses[prepareData.platform.toLowerCase()]; const exitCode = typeof arg === "number" ? arg : arg && arg.code; if (exitCode === 0) { // Non-watch Vite builds spawn the child with stdio:"inherit" @@ -577,6 +579,11 @@ export class BundlerCompilerService this.$options.hostProjectModuleName, USER_PROJECT_PLATFORMS_IOS: this.$options.hostProjectPath, }); + } else if (this.$mobileHelper.ismacOSPlatform(prepareData.platform)) { + // Bundler hardcodes platforms/ios; Catalyst prepares into platforms/macos. + Object.assign(options.env, { + USER_PROJECT_PLATFORMS_IOS: platformData.projectRoot, + }); } if (debugLog) { @@ -589,7 +596,7 @@ export class BundlerCompilerService options, ); - this.bundlerProcesses[platformData.platformNameLowerCase] = childProcess; + this.bundlerProcesses[prepareData.platform.toLowerCase()] = childProcess; await this.$cleanupService.addKillProcess(childProcess.pid.toString()); return childProcess; @@ -778,7 +785,8 @@ export class BundlerCompilerService prepareData: IPrepareData, ) { const { env } = prepareData; - const envData = Object.assign({}, env, { [platform.toLowerCase()]: true }); + const platformKey = platform.toLowerCase(); + const envData = Object.assign({}, env, { [platformKey]: true }); const appId = projectData.projectIdentifiers[platform]; const appPath = projectData.getAppDirectoryRelativePath(); @@ -1042,7 +1050,7 @@ export class BundlerCompilerService hash: lastHash || message.hash, fallbackFiles: [], }, - platform: platformData.platformNameLowerCase, + platform: prepareData.platform.toLowerCase(), }); } diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 550b7dc00d..798eb1a3dc 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -69,12 +69,23 @@ export const DevicePlatformSdkName = "iphoneos"; export const SimulatorPlatformSdkName = "iphonesimulator"; export const VisionDevicePlatformSdkName = "xros"; export const VisionSimulatorPlatformSdkName = "xrsimulator"; +// Not an SDK name — Xcode names the Mac Catalyst products directory +// `-maccatalyst`, and the build output path is derived from it. +export const CatalystPlatformSdkName = "maccatalyst"; const FRAMEWORK_EXTENSIONS = [".framework", ".xcframework"]; const getPlatformSdkName = (buildData: IBuildData): string => { const forDevice = !buildData || buildData.buildForDevice || buildData.buildForAppStore; + + if ( + buildData && + injector.resolve("devicePlatformsConstants").ismacOS(buildData.platform) + ) { + return CatalystPlatformSdkName; + } + const isvisionOS = injector .resolve("devicePlatformsConstants") .isvisionOS(buildData.platform); @@ -152,12 +163,16 @@ export class IOSProjectService (this._platformsDirCache !== projectData.platformsDir || this._platformOverrideCache !== currentOverride) ) { - const platform = this.$mobileHelper.normalizePlatformName( + const requestedPlatform = this.$mobileHelper.normalizePlatformName( this.$options.platformOverride ?? this.$devicePlatformsConstants.iOS, ); + // Mac Catalyst keeps every iOS convention; only the platform root differs. + const platform = this.$mobileHelper.ismacOSPlatform(requestedPlatform) + ? this.$devicePlatformsConstants.iOS + : requestedPlatform; const projectRoot = this.$options.hostProjectPath ? this.$options.hostProjectPath - : path.join(projectData.platformsDir, platform.toLowerCase()); + : path.join(projectData.platformsDir, requestedPlatform.toLowerCase()); const runtimePackage = this.$projectDataService.getRuntimePackage( projectData.projectDir, platform.toLowerCase() as constants.SupportedPlatform, @@ -184,10 +199,12 @@ export class IOSProjectService getValidBuildOutputData: ( buildOptions: IBuildData, ): IValidBuildOutputData => { + // Mac Catalyst produces a .app, never an .ipa. const forDevice = - !buildOptions || - !!buildOptions.buildForDevice || - !!buildOptions.buildForAppStore; + !this.$mobileHelper.ismacOSPlatform(requestedPlatform) && + (!buildOptions || + !!buildOptions.buildForDevice || + !!buildOptions.buildForAppStore); if (forDevice) { const ipaFileName = _.find( this.$fs.readDirectory( @@ -452,7 +469,20 @@ export class IOSProjectService this.emit(constants.BUILD_OUTPUT_EVENT_NAME, data); }; - if (buildData.buildForDevice) { + if (this.$devicePlatformsConstants.ismacOS(buildData.platform)) { + // Signing is handled by `-allowProvisioningUpdates`: Mac Catalyst needs a + // macOS provisioning profile, which the iOS signing service cannot pick. + await attachAwaitDetach( + constants.BUILD_OUTPUT_EVENT_NAME, + this.$childProcess, + handler, + this.$xcodebuildService.buildForCatalyst( + platformData, + projectData, + buildData, + ), + ); + } else if (buildData.buildForDevice) { await this.$iOSSigningService.setupSigningForDevice( projectRoot, projectData, diff --git a/lib/services/ios/xcodebuild-args-service.ts b/lib/services/ios/xcodebuild-args-service.ts index 9f6998b79c..1860772902 100644 --- a/lib/services/ios/xcodebuild-args-service.ts +++ b/lib/services/ios/xcodebuild-args-service.ts @@ -12,6 +12,7 @@ import { IPlatformData } from "../../definitions/platform"; import { IFileSystem } from "../../common/declarations"; import { injector } from "../../common/yok"; import * as _ from "lodash"; +import * as semver from "semver"; import { DevicePlatformSdkName, @@ -21,6 +22,8 @@ import { } from "../ios-project-service"; export class XcodebuildArgsService implements IXcodebuildArgsService { + private static readonly MIN_CATALYST_DEPLOYMENT_TARGET = "13.1"; + constructor( private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $devicesService: Mobile.IDevicesService, @@ -30,6 +33,36 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { private $xcconfigService: IXcconfigService, ) {} + public getBuildForCatalystArgs( + platformData: IPlatformData, + projectData: IProjectData, + buildConfig: IBuildConfig, + ): string[] { + // Forced because the runtime template only sets the legacy UIKITFORMAC alias. + return [ + "-destination", + "platform=macOS,variant=Mac Catalyst", + "build", + "-configuration", + buildConfig.release ? Configurations.Release : Configurations.Debug, + "-allowProvisioningUpdates", + "SUPPORTS_MACCATALYST=YES", + // no `-sdk` here: the destination already selects macOS + the Mac Catalyst + // variant, and forcing an SDK on top of it makes xcodebuild pick iphoneos + "BUILD_DIR=" + path.join(platformData.projectRoot, constants.BUILD_DIR), + "SHARED_PRECOMPS_DIR=" + + path.join(platformData.projectRoot, constants.BUILD_DIR, "sharedpch"), + ] + .concat( + // the deployment target is re-added below, clamped to what Catalyst supports + this + .getXcodeProjectArgs(platformData, projectData) + .filter((arg) => !arg.startsWith("IPHONEOS_DEPLOYMENT_TARGET=")), + ) + .concat(this.getCatalystDeploymentTargetArgs(projectData)) + .concat(this.getBuildLoggingArgs()); + } + public async getBuildForSimulatorArgs( platformData: IPlatformData, projectData: IProjectData, @@ -254,6 +287,44 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { return this.$logger.getLevel() === "INFO" ? ["-quiet"] : []; } + /** + * Mac Catalyst starts at iOS 13.1, so a project that still targets an older iOS + * cannot be built as-is. Raise the deployment target for the Catalyst build only + * rather than failing — the iOS build keeps whatever the app has chosen. + * `MACCATALYST_DEPLOYMENT_TARGET` is passed alongside because the runtime's + * metadata generator reads it and older runtimes crash when it is unset. + */ + private getCatalystDeploymentTargetArgs(projectData: IProjectData): string[] { + const buildSettingsFilePath = path.join( + projectData.appResourcesDirectoryPath, + this.$devicePlatformsConstants.iOS, + constants.BUILD_XCCONFIG_FILE_NAME, + ); + const projectDeploymentTarget = this.$xcconfigService.readPropertyValue( + buildSettingsFilePath, + "IPHONEOS_DEPLOYMENT_TARGET", + ); + const minimum = XcodebuildArgsService.MIN_CATALYST_DEPLOYMENT_TARGET; + let deploymentTarget = projectDeploymentTarget; + + if ( + !deploymentTarget || + semver.lt(semver.coerce(deploymentTarget), semver.coerce(minimum)) + ) { + if (deploymentTarget) { + this.$logger.warn( + `Mac Catalyst requires iOS ${minimum} or higher. Building the Mac Catalyst app with IPHONEOS_DEPLOYMENT_TARGET=${minimum} instead of the project's ${deploymentTarget}.`, + ); + } + deploymentTarget = minimum; + } + + return [ + `IPHONEOS_DEPLOYMENT_TARGET=${deploymentTarget}`, + `MACCATALYST_DEPLOYMENT_TARGET=${deploymentTarget}`, + ]; + } + private getBuildCommonArgs( platformData: IPlatformData, projectData: IProjectData, diff --git a/lib/services/ios/xcodebuild-service.ts b/lib/services/ios/xcodebuild-service.ts index 4f8308dfb5..6cc916ae3f 100644 --- a/lib/services/ios/xcodebuild-service.ts +++ b/lib/services/ios/xcodebuild-service.ts @@ -48,6 +48,22 @@ export class XcodebuildService implements IXcodebuildService { }); } + public async buildForCatalyst( + platformData: IPlatformData, + projectData: IProjectData, + buildConfig: IBuildConfig + ): Promise { + const args = this.$xcodebuildArgsService.getBuildForCatalystArgs( + platformData, + projectData, + buildConfig + ); + await this.$xcodebuildCommandService.executeCommand(args, { + cwd: platformData.projectRoot, + stdio: buildConfig.buildOutputStdio, + }); + } + public async buildForAppStore( platformData: IPlatformData, projectData: IProjectData, diff --git a/lib/services/platform/add-platform-service.ts b/lib/services/platform/add-platform-service.ts index b93aba4ea7..ad947a9095 100644 --- a/lib/services/platform/add-platform-service.ts +++ b/lib/services/platform/add-platform-service.ts @@ -70,11 +70,10 @@ export class AddPlatformService implements IAddPlatformService { return frameworkVersion; } catch (err) { - const platformPath = path.join( - projectData.platformsDir, - platformData.platformNameLowerCase - ); - this.$fs.deleteDirectory(platformPath); + // hostProjectPath is the user's own project; never delete it. + if (!this.$options.hostProjectPath) { + this.$fs.deleteDirectory(platformData.projectRoot); + } throw err; } finally { spinner.stop(); @@ -185,15 +184,8 @@ export class AddPlatformService implements IAddPlatformService { frameworkDirPath: string, frameworkVersion: string ): Promise { - // here we should use ios OR android - const platformDir = - this.$options.hostProjectPath ?? - path.join( - projectData.platformsDir, - platformData.normalizedPlatformName.toLowerCase() - ); - - this.$fs.deleteDirectory(platformDir); + // projectRoot already accounts for hostProjectPath and platforms/macos. + this.$fs.deleteDirectory(platformData.projectRoot); //if iosHost - dont create project await platformData.platformProjectService.createProject( path.resolve(frameworkDirPath), diff --git a/lib/services/platforms-data-service.ts b/lib/services/platforms-data-service.ts index 0f0ac30848..a52c907b5a 100644 --- a/lib/services/platforms-data-service.ts +++ b/lib/services/platforms-data-service.ts @@ -16,6 +16,8 @@ export class PlatformsDataService implements IPlatformsDataService { ios: $iOSProjectService, android: $androidProjectService, visionos: $iOSProjectService, + // Mac Catalyst reuses the iOS project service. + macos: $iOSProjectService, }; } diff --git a/lib/services/project-data-service.ts b/lib/services/project-data-service.ts index 31e5a2a342..bf01980d32 100644 --- a/lib/services/project-data-service.ts +++ b/lib/services/project-data-service.ts @@ -625,6 +625,10 @@ export class ProjectDataService implements IProjectDataService { projectDir: string, platform: constants.SupportedPlatform, ): IBasePluginData { + // Mac Catalyst has no runtime of its own; it uses iOS. + if (platform === constants.PlatformTypes.macos) { + platform = constants.PlatformTypes.ios; + } const runtimePackage = this.$pluginsService .getDependenciesFromPackageJson(projectDir) .devDependencies.find((d) => { From 26860f89c5f11a1bda49ff064398a61970b77547 Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Tue, 11 Aug 2026 17:39:11 +0200 Subject: [PATCH 2/2] refactor(catalyst): name the platform catalyst rather than macos `ns run macos` is being taken by a separate effort that builds a native macOS app against a macOS runtime. That is a different product from a Mac Catalyst build -- the iOS app rebuilt against the macOS SDK -- so this one takes the name that says what it actually is: `ns build catalyst`, `ns run catalyst`, preparing into platforms/catalyst. Only the platform identifier changes; the build, device and LiveSync behaviour is untouched. Co-Authored-By: Claude Opus 5 --- lib/bootstrap.ts | 4 +-- lib/commands/build.ts | 8 ++--- lib/commands/run.ts | 34 +++++++++---------- lib/common/definitions/mobile.d.ts | 6 ++-- .../mobile/device-platforms-constants.ts | 6 ++-- .../mac/mac-catalyst-application-manager.ts | 2 +- lib/common/mobile/mac/mac-catalyst-device.ts | 4 +-- .../mobile-core/ios-device-discovery.ts | 4 +-- .../mobile-core/mac-catalyst-discovery.ts | 2 +- lib/common/mobile/mobile-helper.ts | 12 +++---- lib/constants.ts | 4 +-- lib/controllers/platform-controller.ts | 2 +- lib/controllers/prepare-controller.ts | 2 +- lib/definitions/project.d.ts | 4 +-- lib/device-path-provider.ts | 2 +- lib/project-data.ts | 10 +++--- .../bundler/bundler-compiler-service.ts | 4 +-- lib/services/ios-project-service.ts | 8 ++--- lib/services/platform/add-platform-service.ts | 2 +- lib/services/platforms-data-service.ts | 2 +- lib/services/project-data-service.ts | 2 +- 21 files changed, 62 insertions(+), 62 deletions(-) diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 8a232e9dd9..52e9517562 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -183,7 +183,7 @@ injector.requireCommand("run|ios", "./commands/run"); injector.requireCommand("run|android", "./commands/run"); injector.requireCommand("run|vision", "./commands/run"); injector.requireCommand("run|visionos", "./commands/run"); -injector.requireCommand("run|macos", "./commands/run"); +injector.requireCommand("run|catalyst", "./commands/run"); injector.requireCommand("typings", "./commands/typings"); injector.requireCommand("preview", "./commands/preview"); @@ -199,7 +199,7 @@ injector.requireCommand("build|ios", "./commands/build"); injector.requireCommand("build|android", "./commands/build"); injector.requireCommand("build|vision", "./commands/build"); injector.requireCommand("build|visionos", "./commands/build"); -injector.requireCommand("build|macos", "./commands/build"); +injector.requireCommand("build|catalyst", "./commands/build"); injector.requireCommand("deploy", "./commands/deploy"); injector.requireCommand("embed", "./commands/embedding/embed"); diff --git a/lib/commands/build.ts b/lib/commands/build.ts index fab616d06a..f77ed7685f 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -282,7 +282,7 @@ injector.registerCommand("build|visionos", BuildVisionOsCommand); /** * Builds the iOS target against the macOS SDK as a Mac Catalyst app. */ -export class BuildMacOsCommand extends BuildIosCommand implements ICommand { +export class BuildCatalystCommand extends BuildIosCommand implements ICommand { constructor( protected $options: IOptions, $errors: IErrors, @@ -311,12 +311,12 @@ export class BuildMacOsCommand extends BuildIosCommand implements ICommand { public async execute(args: string[]): Promise { await this.executeCore([ - this.$devicePlatformsConstants.macOS.toLowerCase(), + this.$devicePlatformsConstants.Catalyst.toLowerCase(), ]); } public async canExecute(args: string[]): Promise { - const platform = this.$devicePlatformsConstants.macOS; + const platform = this.$devicePlatformsConstants.Catalyst; if (!this.$options.force) { await this.$migrateController.validate({ projectDir: this.$projectData.projectDir, @@ -335,4 +335,4 @@ export class BuildMacOsCommand extends BuildIosCommand implements ICommand { } } -injector.registerCommand("build|macos", BuildMacOsCommand); +injector.registerCommand("build|catalyst", BuildCatalystCommand); diff --git a/lib/commands/run.ts b/lib/commands/run.ts index e5495277bf..28f7a2552f 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -30,20 +30,20 @@ export class RunCommandBase implements ICommand { private $migrateController: IMigrateController, private $options: IOptions, private $projectData: IProjectData, - private $keyCommandHelper: IKeyCommandHelper + private $keyCommandHelper: IKeyCommandHelper, ) {} public allowedParameters: ICommandParameter[] = []; public async execute(args: string[]): Promise { await this.$liveSyncCommandHelper.executeCommandLiveSync( this.platform, - this.liveSyncCommandHelperAdditionalOptions + this.liveSyncCommandHelperAdditionalOptions, ); if (process.env.NS_IS_INTERACTIVE) { this.$keyCommandHelper.attachKeyCommands( this.platform as IKeyCommandPlatform, - "run" + "run", ); } } @@ -64,7 +64,7 @@ export class RunCommandBase implements ICommand { : [ this.$devicePlatformsConstants.Android, this.$devicePlatformsConstants.iOS, - ]; + ]; if (!this.$options.force) { await this.$migrateController.validate({ @@ -100,7 +100,7 @@ export class RunIosCommand implements ICommand { protected $injector: IInjector, protected $options: IOptions, protected $platformValidationService: IPlatformValidationService, - protected $projectDataService: IProjectDataService + protected $projectDataService: IProjectDataService, ) {} public async execute(args: string[]): Promise { @@ -113,11 +113,11 @@ export class RunIosCommand implements ICommand { if ( !this.$platformValidationService.isPlatformSupportedForOS( this.platform, - projectData + projectData, ) ) { this.$errors.fail( - `Applications for platform ${this.platform} can not be built on this OS` + `Applications for platform ${this.platform} can not be built on this OS`, ); } @@ -127,7 +127,7 @@ export class RunIosCommand implements ICommand { this.$options.provision, this.$options.teamId, projectData, - this.platform.toLowerCase() + this.platform.toLowerCase(), )); return result; } @@ -154,7 +154,7 @@ export class RunAndroidCommand implements ICommand { private $injector: IInjector, private $options: IOptions, private $platformValidationService: IPlatformValidationService, - private $projectData: IProjectData + private $projectData: IProjectData, ) {} public async execute(args: string[]): Promise { @@ -167,11 +167,11 @@ export class RunAndroidCommand implements ICommand { if ( !this.$platformValidationService.isPlatformSupportedForOS( this.$devicePlatformsConstants.Android, - this.$projectData + this.$projectData, ) ) { this.$errors.fail( - `Applications for platform ${this.$devicePlatformsConstants.Android} can not be built on this OS` + `Applications for platform ${this.$devicePlatformsConstants.Android} can not be built on this OS`, ); } @@ -190,7 +190,7 @@ export class RunAndroidCommand implements ICommand { this.$options.provision, this.$options.teamId, this.$projectData, - this.$devicePlatformsConstants.Android.toLowerCase() + this.$devicePlatformsConstants.Android.toLowerCase(), ); } } @@ -208,7 +208,7 @@ export class RunVisionOSCommand extends RunIosCommand { protected $injector: IInjector, protected $options: IOptions, protected $platformValidationService: IPlatformValidationService, - protected $projectDataService: IProjectDataService + protected $projectDataService: IProjectDataService, ) { super( $devicePlatformsConstants, @@ -216,7 +216,7 @@ export class RunVisionOSCommand extends RunIosCommand { $injector, $options, $platformValidationService, - $projectDataService + $projectDataService, ); } } @@ -227,9 +227,9 @@ injector.registerCommand("run|visionos", RunVisionOSCommand); /** * Runs the Mac Catalyst build of the app on this machine. */ -export class RunMacOSCommand extends RunIosCommand { +export class RunCatalystCommand extends RunIosCommand { public get platform(): string { - return this.$devicePlatformsConstants.macOS; + return this.$devicePlatformsConstants.Catalyst; } constructor( @@ -251,4 +251,4 @@ export class RunMacOSCommand extends RunIosCommand { } } -injector.registerCommand("run|macos", RunMacOSCommand); +injector.registerCommand("run|catalyst", RunCatalystCommand); diff --git a/lib/common/definitions/mobile.d.ts b/lib/common/definitions/mobile.d.ts index 009e6e7073..5327dba757 100644 --- a/lib/common/definitions/mobile.d.ts +++ b/lib/common/definitions/mobile.d.ts @@ -1211,7 +1211,7 @@ declare global { isAndroidPlatform(platform: string): boolean; isiOSPlatform(platform: string): boolean; isvisionOSPlatform(platform: string): boolean; - ismacOSPlatform(platform: string): boolean; + isCatalystPlatform(platform: string): boolean; isApplePlatform(platform: string): boolean; normalizePlatformName(platform: string): string; validatePlatformName(platform: string): string; @@ -1256,12 +1256,12 @@ declare global { iOS: string; Android: string; visionOS: string; - macOS: string; + Catalyst: string; isiOS(value: string): boolean; isAndroid(value: string): boolean; isvisionOS(value: string): boolean; - ismacOS(value: string): boolean; + isCatalyst(value: string): boolean; } interface IDeviceApplication { diff --git a/lib/common/mobile/device-platforms-constants.ts b/lib/common/mobile/device-platforms-constants.ts index 5951242343..633a4f0d46 100644 --- a/lib/common/mobile/device-platforms-constants.ts +++ b/lib/common/mobile/device-platforms-constants.ts @@ -7,7 +7,7 @@ export class DevicePlatformsConstants public Android = "Android"; public visionOS = "visionOS"; // Not a runtime of its own: iOS rebuilt against the macOS SDK. - public macOS = "macOS"; + public Catalyst = "Catalyst"; public isiOS(value: string) { return value.toLowerCase() === this.iOS.toLowerCase(); @@ -21,8 +21,8 @@ export class DevicePlatformsConstants return value.toLowerCase() === this.visionOS.toLowerCase(); } - public ismacOS(value: string) { - return value.toLowerCase() === this.macOS.toLowerCase(); + public isCatalyst(value: string) { + return value.toLowerCase() === this.Catalyst.toLowerCase(); } } injector.register("devicePlatformsConstants", DevicePlatformsConstants); diff --git a/lib/common/mobile/mac/mac-catalyst-application-manager.ts b/lib/common/mobile/mac/mac-catalyst-application-manager.ts index ba6e3e0e15..c51e4e1387 100644 --- a/lib/common/mobile/mac/mac-catalyst-application-manager.ts +++ b/lib/common/mobile/mac/mac-catalyst-application-manager.ts @@ -134,7 +134,7 @@ export class MacCatalystApplicationManager extends ApplicationManagerBase { const action = (data: Buffer | string) => { this.$deviceLogProvider.logData( data.toString(), - this.$devicePlatformsConstants.macOS, + this.$devicePlatformsConstants.Catalyst, this.device.deviceInfo.identifier, ); }; diff --git a/lib/common/mobile/mac/mac-catalyst-device.ts b/lib/common/mobile/mac/mac-catalyst-device.ts index 102a41e8f6..71f9a5e4c7 100644 --- a/lib/common/mobile/mac/mac-catalyst-device.ts +++ b/lib/common/mobile/mac/mac-catalyst-device.ts @@ -42,7 +42,7 @@ export class MacCatalystDevice implements Mobile.IMacCatalystDevice { model: "Mac", version: os.release(), vendor: "Apple", - platform: this.$devicePlatformsConstants.macOS, + platform: this.$devicePlatformsConstants.Catalyst, status: constants.CONNECTED_STATUS, errorHelp: null, isTablet: false, @@ -81,7 +81,7 @@ export class MacCatalystDevice implements Mobile.IMacCatalystDevice { private getBuiltApplicationBundlePath(): string { const projectData = this.$projectDataService.getProjectData(); - const platform = this.$devicePlatformsConstants.macOS; + const platform = this.$devicePlatformsConstants.Catalyst; const platformData = this.$platformsDataService.getPlatformData( platform.toLowerCase(), projectData, diff --git a/lib/common/mobile/mobile-core/ios-device-discovery.ts b/lib/common/mobile/mobile-core/ios-device-discovery.ts index 8ff1fef7bd..80405f375b 100644 --- a/lib/common/mobile/mobile-core/ios-device-discovery.ts +++ b/lib/common/mobile/mobile-core/ios-device-discovery.ts @@ -23,8 +23,8 @@ export class IOSDeviceDiscovery extends DeviceDiscovery { options && options.platform && (!this.$mobileHelper.isApplePlatform(options.platform) || - // macOS runs on this machine, not over usbmux. - this.$mobileHelper.ismacOSPlatform(options.platform) || + // Catalyst runs on this machine, not over usbmux. + this.$mobileHelper.isCatalystPlatform(options.platform) || options.emulator) ) { return; diff --git a/lib/common/mobile/mobile-core/mac-catalyst-discovery.ts b/lib/common/mobile/mobile-core/mac-catalyst-discovery.ts index 5dc10e7f96..5c47d7309d 100644 --- a/lib/common/mobile/mobile-core/mac-catalyst-discovery.ts +++ b/lib/common/mobile/mobile-core/mac-catalyst-discovery.ts @@ -26,7 +26,7 @@ export class MacCatalystDeviceDiscovery extends DeviceDiscovery { if ( !options || !options.platform || - !this.$mobileHelper.ismacOSPlatform(options.platform) + !this.$mobileHelper.isCatalystPlatform(options.platform) ) { return; } diff --git a/lib/common/mobile/mobile-helper.ts b/lib/common/mobile/mobile-helper.ts index c32e7ff64e..bb0b4077a0 100644 --- a/lib/common/mobile/mobile-helper.ts +++ b/lib/common/mobile/mobile-helper.ts @@ -21,7 +21,7 @@ export class MobileHelper implements Mobile.IMobileHelper { this.$devicePlatformsConstants.iOS, this.$devicePlatformsConstants.Android, this.$devicePlatformsConstants.visionOS, - this.$devicePlatformsConstants.macOS, + this.$devicePlatformsConstants.Catalyst, ]; } @@ -49,10 +49,10 @@ export class MobileHelper implements Mobile.IMobileHelper { ); } - public ismacOSPlatform(platform: string): boolean { + public isCatalystPlatform(platform: string): boolean { return !!( platform && - this.$devicePlatformsConstants.macOS.toLowerCase() === + this.$devicePlatformsConstants.Catalyst.toLowerCase() === platform.toLowerCase() ); } @@ -61,7 +61,7 @@ export class MobileHelper implements Mobile.IMobileHelper { return ( this.isiOSPlatform(platform) || this.isvisionOSPlatform(platform) || - this.ismacOSPlatform(platform) + this.isCatalystPlatform(platform) ); } @@ -72,8 +72,8 @@ export class MobileHelper implements Mobile.IMobileHelper { return "iOS"; } else if (this.isvisionOSPlatform(platform)) { return "visionOS"; - } else if (this.ismacOSPlatform(platform)) { - return "macOS"; + } else if (this.isCatalystPlatform(platform)) { + return "Catalyst"; } return undefined; diff --git a/lib/constants.ts b/lib/constants.ts index 35821fbdab..196e1e8d88 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -360,14 +360,14 @@ export const enum PlatformTypes { ios = "ios", android = "android", visionos = "visionos", - macos = "macos", + catalyst = "catalyst", } export type SupportedPlatform = | PlatformTypes.ios | PlatformTypes.android | PlatformTypes.visionos - | PlatformTypes.macos; + | PlatformTypes.catalyst; export const PODFILE_NAME = "Podfile"; diff --git a/lib/controllers/platform-controller.ts b/lib/controllers/platform-controller.ts index 8d866bde6b..4142f81332 100644 --- a/lib/controllers/platform-controller.ts +++ b/lib/controllers/platform-controller.ts @@ -188,7 +188,7 @@ export class PlatformController implements IPlatformController { projectData: IProjectData, nativePrepare: INativePrepare ): boolean { - // Mac Catalyst reports iOS but prepares into platforms/macos. + // Mac Catalyst reports iOS but prepares into platforms/catalyst. const platformDirectory = platformData.projectRoot; const platformName = path.basename(platformDirectory); const hasPlatformDirectory = this.$fs.exists(platformDirectory); diff --git a/lib/controllers/prepare-controller.ts b/lib/controllers/prepare-controller.ts index d47d41d692..b0ea888df2 100644 --- a/lib/controllers/prepare-controller.ts +++ b/lib/controllers/prepare-controller.ts @@ -97,7 +97,7 @@ export class PrepareController extends EventEmitter { return this.prepareCore(prepareData, projectData); } - // Catalyst reports iOS in platform data, but events must say macos. + // Catalyst reports iOS in platform data, but events must say catalyst. private getRequestedPlatform(prepareData: IPrepareData): string { return prepareData.platform.toLowerCase(); } diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index 52eaa0d865..deda92a286 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -138,7 +138,7 @@ interface INsConfigIOS extends INsConfigPlaform { interface INSConfigVisionOS extends INsConfigIOS {} -interface INSConfigMacOS extends INsConfigIOS {} +interface INSConfigCatalyst extends INsConfigIOS {} interface INsConfigAndroid extends INsConfigPlaform { v8Flags?: string; @@ -199,7 +199,7 @@ interface INsConfig { ios?: INsConfigIOS; android?: INsConfigAndroid; visionos?: INSConfigVisionOS; - macos?: INSConfigMacOS; + catalyst?: INSConfigCatalyst; ignoredNativeDependencies?: string[]; hooks?: INsConfigHooks[]; projectName?: string; diff --git a/lib/device-path-provider.ts b/lib/device-path-provider.ts index 2f2ba34d4c..adf8e32608 100644 --- a/lib/device-path-provider.ts +++ b/lib/device-path-provider.ts @@ -17,7 +17,7 @@ export class DevicePathProvider implements IDevicePathProvider { options: IDeviceProjectRootOptions ): Promise { let projectRoot = ""; - if (this.$mobileHelper.ismacOSPlatform(device.deviceInfo.platform)) { + if (this.$mobileHelper.isCatalystPlatform(device.deviceInfo.platform)) { projectRoot = (device).applicationBundlePath; if (!projectRoot) { this.$errors.fail("Unable to get application path on device."); diff --git a/lib/project-data.ts b/lib/project-data.ts index 37f9e9e6ef..77bc341e5a 100644 --- a/lib/project-data.ts +++ b/lib/project-data.ts @@ -326,7 +326,7 @@ export class ProjectData implements IProjectData { ios: "", android: "", visionos: "", - macos: "", + catalyst: "", }; } @@ -335,7 +335,7 @@ export class ProjectData implements IProjectData { android: config.id, visionos: config.id, // Mac Catalyst ships under the iOS bundle identifier by default. - macos: config.id, + catalyst: config.id, }; if (config.ios && config.ios.id) { @@ -348,10 +348,10 @@ export class ProjectData implements IProjectData { identifier.visionos = config.visionos.id; } if (config.ios && config.ios.id) { - identifier.macos = config.ios.id; + identifier.catalyst = config.ios.id; } - if (config.macos && config.macos.id) { - identifier.macos = config.macos.id; + if (config.catalyst && config.catalyst.id) { + identifier.catalyst = config.catalyst.id; } return identifier; diff --git a/lib/services/bundler/bundler-compiler-service.ts b/lib/services/bundler/bundler-compiler-service.ts index 710ca1f2b3..94e9a500af 100644 --- a/lib/services/bundler/bundler-compiler-service.ts +++ b/lib/services/bundler/bundler-compiler-service.ts @@ -579,8 +579,8 @@ export class BundlerCompilerService this.$options.hostProjectModuleName, USER_PROJECT_PLATFORMS_IOS: this.$options.hostProjectPath, }); - } else if (this.$mobileHelper.ismacOSPlatform(prepareData.platform)) { - // Bundler hardcodes platforms/ios; Catalyst prepares into platforms/macos. + } else if (this.$mobileHelper.isCatalystPlatform(prepareData.platform)) { + // Bundler hardcodes platforms/ios; Catalyst prepares into platforms/catalyst. Object.assign(options.env, { USER_PROJECT_PLATFORMS_IOS: platformData.projectRoot, }); diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 798eb1a3dc..872af71c4d 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -81,7 +81,7 @@ const getPlatformSdkName = (buildData: IBuildData): string => { if ( buildData && - injector.resolve("devicePlatformsConstants").ismacOS(buildData.platform) + injector.resolve("devicePlatformsConstants").isCatalyst(buildData.platform) ) { return CatalystPlatformSdkName; } @@ -167,7 +167,7 @@ export class IOSProjectService this.$options.platformOverride ?? this.$devicePlatformsConstants.iOS, ); // Mac Catalyst keeps every iOS convention; only the platform root differs. - const platform = this.$mobileHelper.ismacOSPlatform(requestedPlatform) + const platform = this.$mobileHelper.isCatalystPlatform(requestedPlatform) ? this.$devicePlatformsConstants.iOS : requestedPlatform; const projectRoot = this.$options.hostProjectPath @@ -201,7 +201,7 @@ export class IOSProjectService ): IValidBuildOutputData => { // Mac Catalyst produces a .app, never an .ipa. const forDevice = - !this.$mobileHelper.ismacOSPlatform(requestedPlatform) && + !this.$mobileHelper.isCatalystPlatform(requestedPlatform) && (!buildOptions || !!buildOptions.buildForDevice || !!buildOptions.buildForAppStore); @@ -469,7 +469,7 @@ export class IOSProjectService this.emit(constants.BUILD_OUTPUT_EVENT_NAME, data); }; - if (this.$devicePlatformsConstants.ismacOS(buildData.platform)) { + if (this.$devicePlatformsConstants.isCatalyst(buildData.platform)) { // Signing is handled by `-allowProvisioningUpdates`: Mac Catalyst needs a // macOS provisioning profile, which the iOS signing service cannot pick. await attachAwaitDetach( diff --git a/lib/services/platform/add-platform-service.ts b/lib/services/platform/add-platform-service.ts index ad947a9095..87ccb669e9 100644 --- a/lib/services/platform/add-platform-service.ts +++ b/lib/services/platform/add-platform-service.ts @@ -184,7 +184,7 @@ export class AddPlatformService implements IAddPlatformService { frameworkDirPath: string, frameworkVersion: string ): Promise { - // projectRoot already accounts for hostProjectPath and platforms/macos. + // projectRoot already accounts for hostProjectPath and platforms/catalyst. this.$fs.deleteDirectory(platformData.projectRoot); //if iosHost - dont create project await platformData.platformProjectService.createProject( diff --git a/lib/services/platforms-data-service.ts b/lib/services/platforms-data-service.ts index a52c907b5a..59fb9d0801 100644 --- a/lib/services/platforms-data-service.ts +++ b/lib/services/platforms-data-service.ts @@ -17,7 +17,7 @@ export class PlatformsDataService implements IPlatformsDataService { android: $androidProjectService, visionos: $iOSProjectService, // Mac Catalyst reuses the iOS project service. - macos: $iOSProjectService, + catalyst: $iOSProjectService, }; } diff --git a/lib/services/project-data-service.ts b/lib/services/project-data-service.ts index bf01980d32..d8c1198342 100644 --- a/lib/services/project-data-service.ts +++ b/lib/services/project-data-service.ts @@ -626,7 +626,7 @@ export class ProjectDataService implements IProjectDataService { platform: constants.SupportedPlatform, ): IBasePluginData { // Mac Catalyst has no runtime of its own; it uses iOS. - if (platform === constants.PlatformTypes.macos) { + if (platform === constants.PlatformTypes.catalyst) { platform = constants.PlatformTypes.ios; } const runtimePackage = this.$pluginsService