From 7deb5e9a2ea70a0657f7485bebdb670788c8e3fa Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 6 Feb 2026 20:01:50 -0500 Subject: [PATCH 1/5] feat: add macos support --- lib/common/definitions/mobile.d.ts | 3 +++ lib/common/mobile/device-platforms-constants.ts | 5 +++++ lib/common/mobile/mobile-helper.ts | 17 ++++++++++++++++- lib/constants.ts | 5 ++++- lib/definitions/project.d.ts | 2 ++ lib/project-data.ts | 6 ++++++ lib/services/platforms-data-service.ts | 1 + lib/services/project-data-service.ts | 11 ++++++++++- lib/services/versions-service.ts | 6 ++++++ test/services/platform/add-platform-service.ts | 2 +- 10 files changed, 54 insertions(+), 4 deletions(-) diff --git a/lib/common/definitions/mobile.d.ts b/lib/common/definitions/mobile.d.ts index 0d4b58638b..83eca67c63 100644 --- a/lib/common/definitions/mobile.d.ts +++ b/lib/common/definitions/mobile.d.ts @@ -1197,6 +1197,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; @@ -1241,10 +1242,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..c3bc2ac7e2 100644 --- a/lib/common/mobile/device-platforms-constants.ts +++ b/lib/common/mobile/device-platforms-constants.ts @@ -6,6 +6,7 @@ export class DevicePlatformsConstants public iOS = "iOS"; public Android = "Android"; public visionOS = "visionOS"; + public macOS = "macOS"; public isiOS(value: string) { return value.toLowerCase() === this.iOS.toLowerCase(); @@ -18,5 +19,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/mobile-helper.ts b/lib/common/mobile/mobile-helper.ts index 666f2ffae9..f7327a80ac 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 || "macOS", ]; } @@ -48,8 +49,20 @@ export class MobileHelper implements Mobile.IMobileHelper { ); } + public ismacOSPlatform(platform: string): boolean { + const macOSPlatformName = this.$devicePlatformsConstants.macOS || "macOS"; + return !!( + platform && + macOSPlatformName.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 c76d6f6696..0268bd1064 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -24,6 +24,7 @@ export const TNS_IOS_RUNTIME_NAME = "tns-ios"; export const SCOPED_ANDROID_RUNTIME_NAME = "@nativescript/android"; export const SCOPED_IOS_RUNTIME_NAME = "@nativescript/ios"; export const SCOPED_VISIONOS_RUNTIME_NAME = "@nativescript/visionos"; +export const SCOPED_MACOS_RUNTIME_NAME = "@nativescript/macos"; export const PACKAGE_JSON_FILE_NAME = "package.json"; export const PACKAGE_LOCK_JSON_FILE_NAME = "package-lock.json"; export const ANDROID_DEVICE_APP_ROOT_TEMPLATE = `/data/data/%s/files`; @@ -348,12 +349,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/definitions/project.d.ts b/lib/definitions/project.d.ts index c8046b2ed4..0f147a8a2a 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -133,6 +133,7 @@ interface INsConfigIOS extends INsConfigPlaform { } interface INSConfigVisionOS extends INsConfigIOS {} +interface INSConfigMacOS extends INsConfigIOS {} interface INsConfigAndroid extends INsConfigPlaform { v8Flags?: string; @@ -188,6 +189,7 @@ interface INsConfig { ios?: INsConfigIOS; android?: INsConfigAndroid; visionos?: INSConfigVisionOS; + macos?: INSConfigMacOS; ignoredNativeDependencies?: string[]; hooks?: INsConfigHooks[]; projectName?: string; diff --git a/lib/project-data.ts b/lib/project-data.ts index 277dbf32d1..7ccddf91fc 100644 --- a/lib/project-data.ts +++ b/lib/project-data.ts @@ -81,6 +81,7 @@ export class ProjectData implements IProjectData { this.projectIdentifiers.ios = identifier; this.projectIdentifiers.android = identifier; this.projectIdentifiers.visionos = identifier; + this.projectIdentifiers.macos = identifier; } public projectName: string; @@ -326,6 +327,7 @@ export class ProjectData implements IProjectData { ios: "", android: "", visionos: "", + macos: "", }; } @@ -333,6 +335,7 @@ export class ProjectData implements IProjectData { ios: config.id, android: config.id, visionos: config.id, + macos: config.id, }; if (config.ios && config.ios.id) { @@ -344,6 +347,9 @@ export class ProjectData implements IProjectData { if (config.visionos && config.visionos.id) { identifier.visionos = config.visionos.id; } + if (config.macos && config.macos.id) { + identifier.macos = config.macos.id; + } return identifier; } diff --git a/lib/services/platforms-data-service.ts b/lib/services/platforms-data-service.ts index 0f0ac30848..e961b39194 100644 --- a/lib/services/platforms-data-service.ts +++ b/lib/services/platforms-data-service.ts @@ -16,6 +16,7 @@ export class PlatformsDataService implements IPlatformsDataService { ios: $iOSProjectService, android: $androidProjectService, visionos: $iOSProjectService, + macos: $iOSProjectService, }; } diff --git a/lib/services/project-data-service.ts b/lib/services/project-data-service.ts index 52f1ff49a5..3700773679 100644 --- a/lib/services/project-data-service.ts +++ b/lib/services/project-data-service.ts @@ -586,7 +586,9 @@ export class ProjectDataService implements IProjectDataService { const runtimeName = platform === PlatformTypes.android ? constants.TNS_ANDROID_RUNTIME_NAME - : constants.TNS_IOS_RUNTIME_NAME; + : platform === PlatformTypes.macos + ? constants.SCOPED_MACOS_RUNTIME_NAME + : constants.TNS_IOS_RUNTIME_NAME; if ( packageJson && @@ -639,6 +641,8 @@ export class ProjectDataService implements IProjectDataService { ].includes(d.name); } else if (platform === constants.PlatformTypes.visionos) { return d.name === constants.SCOPED_VISIONOS_RUNTIME_NAME; + } else if (platform === constants.PlatformTypes.macos) { + return d.name === constants.SCOPED_MACOS_RUNTIME_NAME; } }); @@ -700,6 +704,11 @@ export class ProjectDataService implements IProjectDataService { name: constants.SCOPED_VISIONOS_RUNTIME_NAME, version: null, }; + } else if (platform === constants.PlatformTypes.macos) { + return { + name: constants.SCOPED_MACOS_RUNTIME_NAME, + version: null, + }; } } diff --git a/lib/services/versions-service.ts b/lib/services/versions-service.ts index 3710ab52bc..38662bea21 100644 --- a/lib/services/versions-service.ts +++ b/lib/services/versions-service.ts @@ -134,6 +134,10 @@ class VersionsService implements IVersionsService { this.projectData.projectDir, constants.PlatformTypes.android ); + const macOSRuntime = this.$projectDataService.getRuntimePackage( + this.projectData.projectDir, + constants.PlatformTypes.macos + ); let runtimes: IBasePluginData[] = []; if (!platform) { @@ -142,6 +146,8 @@ class VersionsService implements IVersionsService { runtimes.push(iosRuntime); } else if (platform === PlatformTypes.android) { runtimes.push(androidRuntime); + } else if (platform === PlatformTypes.macos) { + runtimes.push(macOSRuntime); } const runtimesVersions: IVersionInformation[] = await Promise.all( diff --git a/test/services/platform/add-platform-service.ts b/test/services/platform/add-platform-service.ts index 8519022c4a..952b2fc029 100644 --- a/test/services/platform/add-platform-service.ts +++ b/test/services/platform/add-platform-service.ts @@ -44,7 +44,7 @@ describe("AddPlatformService", () => { projectData = injector.resolve("projectData"); }); - _.each(["ios", "android"], (platform) => { + _.each(["ios", "android", "macos"], (platform) => { it(`should fail if unable to install runtime package for ${platform}`, async () => { const errorMessage = "Pacote service unable to extract package"; From 122d7879d32705a2b6ab98b9e88f9efec2db7bcb Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 11 Feb 2026 13:26:49 -0500 Subject: [PATCH 2/5] feat: implement macOS support with build and run commands --- lib/bootstrap.ts | 2 + lib/commands/build.ts | 55 +++++++++++++++ lib/commands/run.ts | 69 ++++++++++++++++++- lib/controllers/prepare-controller.ts | 51 ++++++++++++++ .../bundler/bundler-compiler-service.ts | 4 ++ lib/services/ios-project-service.ts | 36 ++++++++++ lib/services/ios/xcodebuild-args-service.ts | 40 +++++++++-- lib/services/project-changes-service.ts | 29 ++++++-- 8 files changed, 273 insertions(+), 13 deletions(-) diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 3281d84930..961f861970 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -182,6 +182,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"); @@ -197,6 +198,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 f3c3c1adb3..edc4b2f983 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -277,3 +277,58 @@ export class BuildVisionOsCommand extends BuildIosCommand implements ICommand { injector.registerCommand("build|vision", BuildVisionOsCommand); injector.registerCommand("build|visionos", BuildVisionOsCommand); + +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..37e2dafda3 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -13,9 +13,14 @@ import { ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE, ANDROID_RELEASE_BUILD_ERROR_MESSAGE, } from "../constants"; -import { IOptions, IPlatformValidationService } from "../declarations"; +import { + IOpener, + IOptions, + IPlatformValidationService, +} from "../declarations"; import { IMigrateController } from "../definitions/migrate"; import { IProjectData, IProjectDataService } from "../definitions/project"; +import { IBuildController, IBuildDataService } from "../definitions/build"; export class RunCommandBase implements ICommand { private liveSyncCommandHelperAdditionalOptions: ILiveSyncCommandHelperAdditionalOptions = @@ -223,3 +228,65 @@ export class RunVisionOSCommand extends RunIosCommand { injector.registerCommand("run|vision", RunVisionOSCommand); injector.registerCommand("run|visionos", RunVisionOSCommand); + +export class RunMacOSCommand implements ICommand { + public allowedParameters: ICommandParameter[] = []; + + public get platform(): string { + return this.$devicePlatformsConstants.macOS || "macOS"; + } + + constructor( + private $buildController: IBuildController, + private $buildDataService: IBuildDataService, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $errors: IErrors, + private $migrateController: IMigrateController, + private $opener: IOpener, + private $options: IOptions, + private $platformValidationService: IPlatformValidationService, + private $projectDataService: IProjectDataService, + ) {} + + public async execute(args: string[]): Promise { + const projectData = this.$projectDataService.getProjectData(); + const buildData = this.$buildDataService.getBuildData( + projectData.projectDir, + this.platform.toLowerCase(), + this.$options, + ); + const outputPath = await this.$buildController.prepareAndBuild(buildData); + await this.$opener.open(outputPath, projectData.projectName); + } + + public async canExecute(args: string[]): Promise { + const projectData = this.$projectDataService.getProjectData(); + + if ( + !this.$platformValidationService.isPlatformSupportedForOS( + this.platform, + projectData, + ) + ) { + this.$errors.fail( + `Applications for platform ${this.platform} can not be built on this OS`, + ); + } + + if (!this.$options.force) { + await this.$migrateController.validate({ + projectDir: projectData.projectDir, + platforms: [this.platform], + }); + } + + return this.$platformValidationService.validateOptions( + this.$options.provision, + this.$options.teamId, + projectData, + this.platform.toLowerCase(), + ); + } +} + +injector.registerCommand("run|macos", RunMacOSCommand); diff --git a/lib/controllers/prepare-controller.ts b/lib/controllers/prepare-controller.ts index 73fa9ab5b3..33aef7d035 100644 --- a/lib/controllers/prepare-controller.ts +++ b/lib/controllers/prepare-controller.ts @@ -194,6 +194,8 @@ export class PrepareController extends EventEmitter { }; } + this.syncMacOSBundleArtifacts(projectData, platformData); + await this.writeRuntimePackageJson(projectData, platformData, prepareData); await this.$projectChangesService.savePrepareInfo( @@ -518,6 +520,55 @@ export class PrepareController extends EventEmitter { this.$fs.writeJson(packagePath, packageData); } + private syncMacOSBundleArtifacts( + projectData: IProjectData, + platformData: IPlatformData, + ): void { + if (platformData.platformNameLowerCase !== "macos") { + return; + } + if (process.env.NS_MACOS_IOS_BUNDLE_FALLBACK !== "1") { + return; + } + + const macosAppPath = path.join( + platformData.projectRoot, + projectData.projectName, + "app", + ); + const hasMacBundle = + this.$fs.exists(path.join(macosAppPath, "bundle.js")) || + this.$fs.exists(path.join(macosAppPath, "bundle.mjs")); + + if (hasMacBundle) { + return; + } + + const iosAppPath = path.join( + projectData.platformsDir, + "ios", + projectData.projectName, + "app", + ); + if (!this.$fs.exists(iosAppPath)) { + return; + } + + this.$logger.trace( + `Copying bundle artifacts from ${iosAppPath} to ${macosAppPath} for macOS prepare.`, + ); + + this.$fs.ensureDirectoryExists(macosAppPath); + const emittedFiles = this.$fs.enumerateFilesInDirectorySync(iosAppPath); + + emittedFiles.forEach((sourcePath) => { + const relativePath = path.relative(iosAppPath, sourcePath); + const destinationPath = path.join(macosAppPath, relativePath); + this.$fs.ensureDirectoryExists(path.dirname(destinationPath)); + this.$fs.copyFile(sourcePath, destinationPath); + }); + } + private emitPrepareEvent(filesChangeEventData: IFilesChangeEventData) { if (this.isInitialPrepareReady) { this.emit(PREPARE_READY_EVENT_NAME, filesChangeEventData); diff --git a/lib/services/bundler/bundler-compiler-service.ts b/lib/services/bundler/bundler-compiler-service.ts index 3eb880b2ba..b7f2e91539 100644 --- a/lib/services/bundler/bundler-compiler-service.ts +++ b/lib/services/bundler/bundler-compiler-service.ts @@ -502,6 +502,7 @@ export class BundlerCompilerService USER_PROJECT_PLATFORMS_ANDROID_MODULE: this.$options.hostProjectModuleName, USER_PROJECT_PLATFORMS_IOS: this.$options.hostProjectPath, + USER_PROJECT_PLATFORMS_MACOS: this.$options.hostProjectPath, }); } @@ -528,6 +529,9 @@ export class BundlerCompilerService ) { const { env } = prepareData; const envData = Object.assign({}, env, { [platform.toLowerCase()]: true }); + if (platform.toLowerCase() === "macos") { + envData.platform = "macos"; + } const appId = projectData.projectIdentifiers[platform]; const appPath = projectData.getAppDirectoryRelativePath(); diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index bfd37bf216..2493e858d4 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -69,6 +69,7 @@ export const DevicePlatformSdkName = "iphoneos"; export const SimulatorPlatformSdkName = "iphonesimulator"; export const VisionDevicePlatformSdkName = "xros"; export const VisionSimulatorPlatformSdkName = "xrsimulator"; +export const MacOSPlatformSdkName = "macosx"; const FRAMEWORK_EXTENSIONS = [".framework", ".xcframework"]; @@ -78,6 +79,9 @@ const getPlatformSdkName = (buildData: IBuildData): string => { const isvisionOS = injector .resolve("devicePlatformsConstants") .isvisionOS(buildData.platform); + const ismacOS = injector + .resolve("devicePlatformsConstants") + .ismacOS(buildData.platform); if (isvisionOS) { return forDevice @@ -85,6 +89,10 @@ const getPlatformSdkName = (buildData: IBuildData): string => { : VisionSimulatorPlatformSdkName; } + if (ismacOS) { + return MacOSPlatformSdkName; + } + return forDevice ? DevicePlatformSdkName : SimulatorPlatformSdkName; }; @@ -155,6 +163,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ platform.toLowerCase() as constants.SupportedPlatform, ); + const isMacOSPlatform = this.$devicePlatformsConstants.ismacOS(platform); this._platformData = { frameworkPackageName: runtimePackage.name, normalizedPlatformName: platform, @@ -167,6 +176,9 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ projectRoot: projectRoot, getBuildOutputPath: (options: IBuildData): string => { const config = getConfigurationName(!options || options.release); + if (isMacOSPlatform) { + return path.join(projectRoot, constants.BUILD_DIR, config); + } return path.join( projectRoot, constants.BUILD_DIR, @@ -176,6 +188,17 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ getValidBuildOutputData: ( buildOptions: IBuildData, ): IValidBuildOutputData => { + const isMacOS = + !!buildOptions && + this.$devicePlatformsConstants.ismacOS(buildOptions.platform); + if (isMacOS) { + return { + packageNames: [ + `${projectData.projectName}.app`, + `${projectData.projectName}.zip`, + ], + }; + } const forDevice = !buildOptions || !!buildOptions.buildForDevice || @@ -942,6 +965,19 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ this.$fs.deleteDirectory( path.join(platformsAppResourcesPath, "watchextension"), ); + + const macOSPlatformName = + this.$devicePlatformsConstants.macOS && + this.$devicePlatformsConstants.macOS.toLowerCase(); + if ( + macOSPlatformName && + platformData.platformNameLowerCase === macOSPlatformName + ) { + // macOS apps do not use iOS launch storyboards. + this.$fs.deleteFile( + path.join(platformsAppResourcesPath, "LaunchScreen.storyboard"), + ); + } } public async processConfigurationFilesFromAppResources( diff --git a/lib/services/ios/xcodebuild-args-service.ts b/lib/services/ios/xcodebuild-args-service.ts index 204c066423..9bcb4f3705 100644 --- a/lib/services/ios/xcodebuild-args-service.ts +++ b/lib/services/ios/xcodebuild-args-service.ts @@ -18,6 +18,7 @@ import { SimulatorPlatformSdkName, VisionDevicePlatformSdkName, VisionSimulatorPlatformSdkName, + MacOSPlatformSdkName, } from "../ios-project-service"; export class XcodebuildArgsService implements IXcodebuildArgsService { @@ -37,7 +38,13 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { ): Promise { let args = await this.getArchitecturesArgs(buildConfig); - if (this.$iOSWatchAppService.hasWatchApp(platformData, projectData)) { + const isMacOS = this.$devicePlatformsConstants.ismacOS( + buildConfig.platform, + ); + + if (isMacOS) { + args = args.concat(["CODE_SIGNING_ALLOWED=NO"]); + } else if (this.$iOSWatchAppService.hasWatchApp(platformData, projectData)) { args = args.concat(["CODE_SIGNING_ALLOWED=NO"]); } else { args = args.concat(["CODE_SIGN_IDENTITY="]); @@ -49,6 +56,10 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { buildConfig.platform, ); + if (isMacOS) { + destination = "generic/platform=macOS"; + } + if (isvisionOS) { destination = "generic/platform=visionOS Simulator"; if (buildConfig._device) { @@ -68,9 +79,11 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { this.getBuildCommonArgs( platformData, projectData, - isvisionOS - ? VisionSimulatorPlatformSdkName - : SimulatorPlatformSdkName, + isMacOS + ? MacOSPlatformSdkName + : isvisionOS + ? VisionSimulatorPlatformSdkName + : SimulatorPlatformSdkName, ), ) .concat(this.getBuildLoggingArgs()) @@ -93,6 +106,11 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { let isvisionOS = this.$devicePlatformsConstants.isvisionOS( buildConfig.platform, ); + const isMacOS = this.$devicePlatformsConstants.ismacOS(buildConfig.platform); + + if (isMacOS) { + destination = "generic/platform=macOS"; + } if (isvisionOS) { destination = "generic/platform=visionOS"; @@ -116,7 +134,11 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { this.getBuildCommonArgs( platformData, projectData, - isvisionOS ? VisionDevicePlatformSdkName : DevicePlatformSdkName, + isMacOS + ? MacOSPlatformSdkName + : isvisionOS + ? VisionDevicePlatformSdkName + : DevicePlatformSdkName, ), ) .concat(this.getBuildLoggingArgs()); @@ -136,6 +158,14 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { return args; } + if (this.$devicePlatformsConstants.ismacOS(buildConfig.platform)) { + if (process.arch === "arm64") { + // Avoid attempting x86_64 builds against arm64-only frameworks. + args.push("ONLY_ACTIVE_ARCH=YES", "EXCLUDED_ARCHS=x86_64"); + } + return args; + } + const devicesArchitectures = buildConfig.buildForDevice ? await this.getArchitecturesFromConnectedDevices(buildConfig) : []; diff --git a/lib/services/project-changes-service.ts b/lib/services/project-changes-service.ts index 12e7574bfc..dc7ccdb585 100644 --- a/lib/services/project-changes-service.ts +++ b/lib/services/project-changes-service.ts @@ -89,22 +89,37 @@ export class ProjectChangesService implements IProjectChangesService { projectData.appResourcesDirectoryPath, platformData.normalizedPlatformName ); + const visionOSPlatformName = + this.$devicePlatformsConstants.visionOS && + this.$devicePlatformsConstants.visionOS.toLowerCase(); + const macOSPlatformName = + this.$devicePlatformsConstants.macOS && + this.$devicePlatformsConstants.macOS.toLowerCase(); + const iOSPlatformName = + this.$devicePlatformsConstants.iOS || "iOS"; if ( !this.$fs.exists(platformResourcesDir) && - platformData.platformNameLowerCase === - this.$devicePlatformsConstants.visionOS.toLowerCase() + (platformData.platformNameLowerCase === visionOSPlatformName || + platformData.platformNameLowerCase === macOSPlatformName) ) { platformResourcesDir = path.join( projectData.appResourcesDirectoryPath, - this.$devicePlatformsConstants.iOS + iOSPlatformName ); } - this._changesInfo.appResourcesChanged = this.containsNewerFiles( - platformResourcesDir, - projectData - ); + if (this.$fs.exists(platformResourcesDir)) { + this._changesInfo.appResourcesChanged = this.containsNewerFiles( + platformResourcesDir, + projectData + ); + } else { + this.$logger.trace( + `App resources path does not exist: ${platformResourcesDir}` + ); + this._changesInfo.appResourcesChanged = false; + } this.$nodeModulesDependenciesBuilder .getProductionDependencies( From 225725691673b54d5ee376b5faaec11e9beb3ed7 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Thu, 12 Feb 2026 22:16:52 -0500 Subject: [PATCH 3/5] feat: add macOS command support for opening projects in Xcode --- lib/key-commands/bootstrap.ts | 1 + lib/key-commands/index.ts | 61 ++++++++++++++ .../bundler/bundler-compiler-service.ts | 13 ++- lib/services/ios-project-service.ts | 80 ++++++++++++++++--- lib/services/ios/spm-service.ts | 9 ++- lib/services/plugins-service.ts | 47 ++++++++++- lib/services/project-data-service.ts | 18 +++-- 7 files changed, 205 insertions(+), 24 deletions(-) diff --git a/lib/key-commands/bootstrap.ts b/lib/key-commands/bootstrap.ts index a10d2bd7b8..9afe3ea281 100644 --- a/lib/key-commands/bootstrap.ts +++ b/lib/key-commands/bootstrap.ts @@ -21,3 +21,4 @@ injector.requireCommand("open|ios", path); injector.requireCommand("open|android", path); injector.requireCommand("open|visionos", path); injector.requireCommand("open|vision", path); +injector.requireCommand("open|macos", path); diff --git a/lib/key-commands/index.ts b/lib/key-commands/index.ts index 904e3bf3b8..05f3373381 100644 --- a/lib/key-commands/index.ts +++ b/lib/key-commands/index.ts @@ -346,6 +346,66 @@ export class OpenVisionOSCommand extends ShiftV { } } +export class OpenMacOSCommand implements IKeyCommand { + platform: IKeyCommandPlatform = "all"; + description: string = "Open project in Xcode"; + group = "macOS"; + willBlockKeyCommandExecution: boolean = true; + protected isInteractive: boolean = false; + + constructor( + private $iOSProjectService: IOSProjectService, + private $logger: ILogger, + private $childProcess: IChildProcess, + private $projectData: IProjectData, + private $xcodeSelectService: IXcodeSelectService, + private $xcodebuildArgsService: IXcodebuildArgsService, + private $options: IOptions + ) {} + + async execute(): Promise { + this.$options.watch = false; + this.$options.platformOverride = "macOS"; + const os = currentPlatform(); + if (os === "darwin") { + this.$projectData.initializeProjectData(); + const macOSDir = path.resolve(this.$projectData.platformsDir, "macos"); + + if (!fs.existsSync(macOSDir)) { + const prepareCommand = injector.resolveCommand( + "prepare" + ) as PrepareCommand; + + await prepareCommand.execute(["macos"]); + if (this.isInteractive) { + process.stdin.resume(); + } + } + const platformData = this.$iOSProjectService.getPlatformData( + this.$projectData + ); + const xcprojectFile = this.$xcodebuildArgsService.getXcodeProjectArgs( + platformData, + this.$projectData + )[1]; + + if (fs.existsSync(xcprojectFile)) { + this.$xcodeSelectService + .getDeveloperDirectoryPath() + .then(() => this.$childProcess.exec(`open ${xcprojectFile}`, {})) + .catch((e) => { + this.$logger.error(e.message); + }); + } else { + this.$logger.error(`Unable to open project file: ${xcprojectFile}`); + } + } else { + this.$logger.error("Opening a project in XCode requires macOS."); + } + this.$options.platformOverride = null; + } +} + export class R implements IKeyCommand { key: IValidKeyName = "r"; platform: IKeyCommandPlatform = "all"; @@ -520,3 +580,4 @@ injector.registerCommand("open|ios", OpenIOSCommand); injector.registerCommand("open|visionos", OpenVisionOSCommand); injector.registerCommand("open|vision", OpenVisionOSCommand); injector.registerCommand("open|android", OpenAndroidCommand); +injector.registerCommand("open|macos", OpenMacOSCommand); diff --git a/lib/services/bundler/bundler-compiler-service.ts b/lib/services/bundler/bundler-compiler-service.ts index b7f2e91539..ab3f266ea2 100644 --- a/lib/services/bundler/bundler-compiler-service.ts +++ b/lib/services/bundler/bundler-compiler-service.ts @@ -407,7 +407,16 @@ export class BundlerCompilerService } } - private async shouldUsePreserveSymlinksOption(): Promise { + private async shouldUsePreserveSymlinksOption( + projectData: IProjectData, + ): Promise { + // Modern bundlers (webpack v5+ and rspack) are more sensitive to + // duplicate module identities when symlink paths are preserved. + // In local file-linked setups this can load webpack internals twice. + if (this.isModernBundler(projectData)) { + return false; + } + // pnpm does not require symlink (https://github.com/nodejs/node-eps/issues/46#issuecomment-277373566) // and it also does not work in some cases. // Check https://github.com/NativeScript/nativescript-cli/issues/5259 for more information @@ -461,7 +470,7 @@ export class BundlerCompilerService const additionalNodeArgs = semver.major(process.version) <= 8 ? ["--harmony"] : []; - if (await this.shouldUsePreserveSymlinksOption()) { + if (await this.shouldUsePreserveSymlinksOption(projectData)) { additionalNodeArgs.push("--preserve-symlinks"); } diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 2493e858d4..3ce04d0418 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -102,7 +102,6 @@ const getConfigurationName = (release: boolean): string => { export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase { private static IOS_PROJECT_NAME_PLACEHOLDER = "__PROJECT_NAME__"; - private static IOS_PLATFORM_NAME = "ios"; constructor( $fs: IFileSystem, @@ -259,6 +258,14 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ return this._platformData; } + private getPluginPlatform(projectData: IProjectData): string { + const platformData = this.getPlatformData(projectData); + return ( + platformData.normalizedPlatformName?.toLowerCase() || + platformData.platformNameLowerCase + ); + } + public async validateOptions( projectId: string, provision: true | string, @@ -1028,9 +1035,10 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ }; const allPlugins = this.getAllProductionPlugins(projectData); + const pluginPlatform = this.getPluginPlatform(projectData); for (const plugin of allPlugins) { const pluginInfoPlistPath = path.join( - plugin.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME), + plugin.pluginPlatformsFolderPath(pluginPlatform), this.getPlatformData(projectData).configurationFileName, ); makePatch(pluginInfoPlistPath); @@ -1171,10 +1179,14 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ opts?: any, ): Promise { const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath( - IOSProjectService.IOS_PLATFORM_NAME, + this.getPluginPlatform(projectData), ); - const sourcePath = path.join(pluginPlatformsFolderPath, "src"); + const sourcePath = this.getPluginNativeSourcePath( + pluginData, + projectData, + pluginPlatformsFolderPath + ); await this.prepareNativeSourceCode( pluginData.name, sourcePath, @@ -1203,11 +1215,16 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ projectData: IProjectData, ): Promise { const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath( - IOSProjectService.IOS_PLATFORM_NAME, + this.getPluginPlatform(projectData), + ); + const sourcePath = this.getPluginNativeSourcePath( + pluginData, + projectData, + pluginPlatformsFolderPath ); this.removeNativeSourceCode( - pluginPlatformsFolderPath, + sourcePath, pluginData, projectData, ); @@ -1384,6 +1401,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ private getAllLibsForPluginWithFileExtension( pluginData: IPluginData, fileExtension: string | string[], + projectData: IProjectData, ): string[] { const fileExtensions = _.isArray(fileExtension) ? fileExtension @@ -1394,7 +1412,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ ) => fileExtensions.indexOf(path.extname(fileName)) !== -1; return this.getAllNativeLibrariesForPlugin( pluginData, - IOSProjectService.IOS_PLATFORM_NAME, + this.getPluginPlatform(projectData), filterCallback, ); } @@ -1476,6 +1494,36 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ this.savePbxProj(project, projectData); } + private getPluginNativeSourcePath( + pluginData: IPluginData, + projectData: IProjectData, + pluginPlatformsFolderPath: string + ): string { + const sourcePath = path.join(pluginPlatformsFolderPath, "src"); + + if (this.$fs.exists(sourcePath)) { + return sourcePath; + } + + // For macOS only, allow reusing plugin native source code from iOS. + // Framework/static library discovery remains strict to `platforms/macos`. + if ( + this.$devicePlatformsConstants.ismacOS( + this.getPlatformData(projectData).normalizedPlatformName + ) + ) { + const iosSourcePath = path.join( + pluginData.pluginPlatformsFolderPath(constants.PlatformTypes.ios), + "src" + ); + if (this.$fs.exists(iosSourcePath)) { + return iosSourcePath; + } + } + + return sourcePath; + } + private async addExtensions( projectData: IProjectData, pluginsData: IPluginData[], @@ -1498,7 +1546,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ for (const pluginIndex in pluginsData) { const pluginData = pluginsData[pluginIndex]; const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath( - IOSProjectService.IOS_PLATFORM_NAME, + this.getPluginPlatform(projectData), ); const extensionPath = path.join( @@ -1577,6 +1625,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ for (const fileName of this.getAllLibsForPluginWithFileExtension( pluginData, FRAMEWORK_EXTENSIONS, + projectData, )) { await this.addFramework( path.join(pluginPlatformsFolderPath, fileName), @@ -1593,6 +1642,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ for (const fileName of this.getAllLibsForPluginWithFileExtension( pluginData, ".a", + projectData, )) { await this.addStaticLibrary( path.join(pluginPlatformsFolderPath, fileName), @@ -1602,14 +1652,14 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } private async removeNativeSourceCode( - pluginPlatformsFolderPath: string, + sourceFolderPath: string, pluginData: IPluginData, projectData: IProjectData, ) { const project = this.createPbxProj(projectData); const group = await this.getRootGroup( pluginData.name, - pluginPlatformsFolderPath, + sourceFolderPath, ); project.removePbxGroup(group.name, group.path); project.removeFromHeaderSearchPaths(group.path); @@ -1626,6 +1676,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ this.getAllLibsForPluginWithFileExtension( pluginData, FRAMEWORK_EXTENSIONS, + projectData, ), (fileName) => { const relativeFrameworkPath = this.getLibSubpathRelativeToProjectPath( @@ -1650,7 +1701,11 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ const project = this.createPbxProj(projectData); _.each( - this.getAllLibsForPluginWithFileExtension(pluginData, ".a"), + this.getAllLibsForPluginWithFileExtension( + pluginData, + ".a", + projectData, + ), (fileName) => { const staticLibPath = path.join(pluginPlatformsFolderPath, fileName); const relativeStaticLibPath = this.getLibSubpathRelativeToProjectPath( @@ -1718,9 +1773,10 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } const allPlugins: IPluginData[] = this.getAllProductionPlugins(projectData); + const pluginPlatform = this.getPluginPlatform(projectData); for (const plugin of allPlugins) { const pluginPlatformsFolderPath = plugin.pluginPlatformsFolderPath( - IOSProjectService.IOS_PLATFORM_NAME, + pluginPlatform, ); const pluginXcconfigFilePath = path.join( pluginPlatformsFolderPath, diff --git a/lib/services/ios/spm-service.ts b/lib/services/ios/spm-service.ts index c7462b9d8e..5412186f4f 100644 --- a/lib/services/ios/spm-service.ts +++ b/lib/services/ios/spm-service.ts @@ -116,12 +116,19 @@ export class SPMService implements ISPMService { platformData: IPlatformData, projectData: IProjectData, ) { + let destination = "generic/platform=iOS"; + if (platformData.platformNameLowerCase === "visionos") { + destination = "generic/platform=visionOS"; + } else if (platformData.platformNameLowerCase === "macos") { + destination = "generic/platform=macOS"; + } + await this.$xcodebuildCommandService.executeCommand( this.$xcodebuildArgsService .getXcodeProjectArgs(platformData, projectData) .concat([ "-destination", - "generic/platform=iOS", + destination, "-resolvePackageDependencies", ]), { diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index 2fadc8fe16..7da82604e3 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -242,7 +242,12 @@ export class PluginsService implements IPluginsService { const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(platform); - if (this.$fs.exists(pluginPlatformsFolderPath)) { + const pluginNativeFilesPath = this.getPluginNativeFilesPath( + pluginData, + platform, + pluginPlatformsFolderPath + ); + if (pluginNativeFilesPath) { const pathToPluginsBuildFile = path.join( platformData.projectRoot, constants.PLUGINS_BUILD_DATA_FILENAME @@ -253,7 +258,7 @@ export class PluginsService implements IPluginsService { ); const oldPluginNativeHashes = allPluginsNativeHashes[pluginData.name]; const currentPluginNativeHashes = await this.getPluginNativeHashes( - pluginPlatformsFolderPath + pluginNativeFilesPath ); if ( @@ -269,7 +274,7 @@ export class PluginsService implements IPluginsService { ); const updatedPluginNativeHashes = await this.getPluginNativeHashes( - pluginPlatformsFolderPath + pluginNativeFilesPath ); this.setPluginNativeHashes({ @@ -282,6 +287,30 @@ export class PluginsService implements IPluginsService { } } + private getPluginNativeFilesPath( + pluginData: IPluginData, + platform: string, + pluginPlatformsFolderPath: string + ): string | null { + if (this.$fs.exists(pluginPlatformsFolderPath)) { + return pluginPlatformsFolderPath; + } + + if (this.$mobileHelper.ismacOSPlatform(platform)) { + const iosSourcePath = path.join( + pluginData.pluginPlatformsFolderPath(constants.PlatformTypes.ios), + "src" + ); + if (this.$fs.exists(iosSourcePath)) { + // For macOS, allow iOS native source fallback only. + // Binary framework/static library discovery remains strict to `platforms/macos`. + return iosSourcePath; + } + } + + return null; + } + public async ensureAllDependenciesAreInstalled( projectData: IProjectData ): Promise { @@ -637,6 +666,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple if (this.$mobileHelper.isvisionOSPlatform(platform)) { platform = constants.PlatformTypes.ios; } + return path.join( pluginData.fullPath, "platforms", @@ -842,7 +872,16 @@ This framework comes from ${dependencyName} plugin, which is installed multiple ); const pluginPlatformsData = pluginData.platformsData; if (pluginPlatformsData) { - const versionRequiredByPlugin = (pluginPlatformsData)[platform]; + let versionRequiredByPlugin = (pluginPlatformsData)[platform]; + if ( + !versionRequiredByPlugin && + this.$mobileHelper.ismacOSPlatform(platform) + ) { + // Keep macOS compatible with existing iOS plugin declarations. + versionRequiredByPlugin = (pluginPlatformsData)[ + constants.PlatformTypes.ios + ]; + } if (!versionRequiredByPlugin) { this.$logger.warn( `${pluginData.name} is not supported for ${platform}.` diff --git a/lib/services/project-data-service.ts b/lib/services/project-data-service.ts index 3700773679..24b40bb1c0 100644 --- a/lib/services/project-data-service.ts +++ b/lib/services/project-data-service.ts @@ -647,6 +647,7 @@ export class ProjectDataService implements IProjectDataService { }); if (runtimePackage) { + const originalVersion = runtimePackage.version; const coerced = semver.coerce(runtimePackage.version); const isRange = !!coerced && coerced.version !== runtimePackage.version; const isTag = !coerced; @@ -669,11 +670,18 @@ export class ProjectDataService implements IProjectDataService { runtimePackage.version = this.$fs.readJson( runtimePackageJsonPath ).version; - } catch (err) { - if (isRange) { - runtimePackage.version = semver.coerce( - runtimePackage.version - ).version; + } catch (err) { + // Keep local file/tgz runtime specs even if the package is not yet + // installed in node_modules. `npm install @` works. + if ( + originalVersion?.startsWith("file:") || + originalVersion?.includes("tgz") + ) { + runtimePackage.version = originalVersion; + } else if (isRange) { + runtimePackage.version = semver.coerce( + runtimePackage.version + ).version; (runtimePackage as any)._coerced = true; } else { From d08a319e4f0f106ecf962ffd276732ed531f893a Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 13 Feb 2026 19:08:39 -0500 Subject: [PATCH 4/5] feat: Add 'M' key binding for the `OpenMacOSCommand` to open project in Xcode. --- lib/key-commands/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/key-commands/index.ts b/lib/key-commands/index.ts index 05f3373381..47b7fec7c0 100644 --- a/lib/key-commands/index.ts +++ b/lib/key-commands/index.ts @@ -347,6 +347,7 @@ export class OpenVisionOSCommand extends ShiftV { } export class OpenMacOSCommand implements IKeyCommand { + key: IValidKeyName = "M"; platform: IKeyCommandPlatform = "all"; description: string = "Open project in Xcode"; group = "macOS"; From f092337764f8138a45b1f1f26b9306db47c0ceb5 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Fri, 13 Feb 2026 19:52:09 -0800 Subject: [PATCH 5/5] chore: 9.1.0-alpha.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5881ad3369..5b94e022f3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nativescript", "main": "./lib/nativescript-cli-lib.js", - "version": "9.0.3", + "version": "9.1.0-alpha.0", "author": "NativeScript ", "description": "Command-line interface for building NativeScript projects", "bin": {