diff --git a/PublicAPI.md b/PublicAPI.md index 066640eb0d..1dd590f933 100644 --- a/PublicAPI.md +++ b/PublicAPI.md @@ -656,11 +656,6 @@ interface IDebugData { */ applicationIdentifier: string; - /** - * Path to .app built for iOS Simulator. - */ - pathToAppPackage?: string; - /** * The name of the application, for example `MyProject`. */ diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index a8eda19224..6e24271e22 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -32,10 +32,32 @@ $injector.require("projectTemplatesService", "./services/project-templates-servi $injector.require("projectNameService", "./services/project-name-service"); $injector.require("tnsModulesService", "./services/tns-modules-service"); -$injector.require("platformsData", "./platforms-data"); -$injector.require("platformService", "./services/platform-service"); -$injector.require("preparePlatformJSService", "./services/prepare-platform-js-service"); -$injector.require("preparePlatformNativeService", "./services/prepare-platform-native-service"); +$injector.require("platformsDataService", "./services/platforms-data-service"); +$injector.require("addPlatformService", "./services/platform/add-platform-service"); +$injector.require("buildInfoFileService", "./services/build-info-file-service"); +$injector.require("prepareNativePlatformService", "./services/platform/prepare-native-platform-service"); +$injector.require("platformValidationService", "./services/platform/platform-validation-service"); + +$injector.require("buildArtefactsService", "./services/build-artefacts-service"); + +$injector.require("deviceDebugAppService", "./services/device/device-debug-app-service"); +$injector.require("deviceInstallAppService", "./services/device/device-install-app-service"); +$injector.require("deviceRefreshAppService", "./services/device/device-refresh-app-service"); + +$injector.require("runEmitter", "./emitters/run-emitter"); +$injector.require("previewAppEmitter", "./emitters/preview-app-emitter"); + +$injector.require("platformController", "./controllers/platform-controller"); +$injector.require("prepareController", "./controllers/prepare-controller"); +$injector.require("buildController", "./controllers/build-controller"); +$injector.require("deployController", "./controllers/deploy-controller"); +$injector.require("runController", "./controllers/run-controller"); +$injector.require("previewAppController", "./controllers/preview-app-controller"); + +$injector.require("prepareDataService", "./services/prepare-data-service"); +$injector.require("buildDataService", "./services/build-data-service"); + +$injector.require("liveSyncServiceResolver", "./resolvers/livesync-service-resolver"); $injector.require("debugDataService", "./services/debug-data-service"); $injector.requirePublicClass("debugService", "./services/debug-service"); @@ -47,8 +69,6 @@ $injector.requirePublic("analyticsSettingsService", "./services/analytics-settin $injector.require("analyticsService", "./services/analytics/analytics-service"); $injector.require("googleAnalyticsProvider", "./services/analytics/google-analytics-provider"); -$injector.require("emulatorSettingsService", "./services/emulator-settings-service"); - $injector.require("platformCommandParameter", "./platform-command-param"); $injector.requireCommand("create", "./commands/create-project"); $injector.requireCommand("generate", "./commands/generate"); @@ -66,8 +86,6 @@ $injector.requireCommand("debug|ios", "./commands/debug"); $injector.requireCommand("debug|android", "./commands/debug"); $injector.requireCommand("prepare", "./commands/prepare"); -$injector.requireCommand("clean-app|ios", "./commands/clean-app"); -$injector.requireCommand("clean-app|android", "./commands/clean-app"); $injector.requireCommand("build|ios", "./commands/build"); $injector.requireCommand("build|android", "./commands/build"); $injector.requireCommand("deploy", "./commands/deploy"); @@ -134,10 +152,10 @@ $injector.require("bundleValidatorHelper", "./helpers/bundle-validator-helper"); $injector.require("androidBundleValidatorHelper", "./helpers/android-bundle-validator-helper"); $injector.require("liveSyncCommandHelper", "./helpers/livesync-command-helper"); $injector.require("deployCommandHelper", "./helpers/deploy-command-helper"); +$injector.require("platformCommandHelper", "./helpers/platform-command-helper"); $injector.require("optionsTracker", "./helpers/options-track-helper"); $injector.requirePublicClass("localBuildService", "./services/local-build-service"); -$injector.requirePublicClass("liveSyncService", "./services/livesync/livesync-service"); $injector.require("LiveSyncSocket", "./services/livesync/livesync-socket"); $injector.requirePublicClass("androidLivesyncTool", "./services/livesync/android-livesync-tool"); $injector.require("androidLiveSyncService", "./services/livesync/android-livesync-service"); @@ -201,3 +219,5 @@ $injector.require("testInitializationService", "./services/test-initialization-s $injector.require("networkConnectivityValidator", "./helpers/network-connectivity-validator"); $injector.requirePublic("cleanupService", "./services/cleanup-service"); + +$injector.require("webpackCompilerService", "./services/webpack/webpack-compiler-service"); diff --git a/lib/commands/add-platform.ts b/lib/commands/add-platform.ts index a75cff15bd..b21878006d 100644 --- a/lib/commands/add-platform.ts +++ b/lib/commands/add-platform.ts @@ -4,16 +4,17 @@ export class AddPlatformCommand extends ValidatePlatformCommandBase implements I public allowedParameters: ICommandParameter[] = []; constructor($options: IOptions, - $platformService: IPlatformService, + private $platformCommandHelper: IPlatformCommandHelper, + $platformValidationService: IPlatformValidationService, $projectData: IProjectData, - $platformsData: IPlatformsData, + $platformsDataService: IPlatformsDataService, private $errors: IErrors) { - super($options, $platformsData, $platformService, $projectData); + super($options, $platformsDataService, $platformValidationService, $projectData); this.$projectData.initializeProjectData(); } public async execute(args: string[]): Promise { - await this.$platformService.addPlatforms(args, this.$projectData, this.$options, this.$options.frameworkPath); + await this.$platformCommandHelper.addPlatforms(args, this.$projectData, this.$options.frameworkPath); } public async canExecute(args: string[]): Promise { @@ -23,9 +24,9 @@ export class AddPlatformCommand extends ValidatePlatformCommandBase implements I let canExecute = true; for (const arg of args) { - this.$platformService.validatePlatform(arg, this.$projectData); + this.$platformValidationService.validatePlatform(arg, this.$projectData); - if (!this.$platformService.isPlatformSupportedForOS(arg, this.$projectData)) { + if (!this.$platformValidationService.isPlatformSupportedForOS(arg, this.$projectData)) { this.$errors.fail(`Applications for platform ${arg} can not be built on this OS`); } diff --git a/lib/commands/appstore-list.ts b/lib/commands/appstore-list.ts index 6313a2f64b..128d9b8a3f 100644 --- a/lib/commands/appstore-list.ts +++ b/lib/commands/appstore-list.ts @@ -9,14 +9,14 @@ export class ListiOSApps implements ICommand { private $logger: ILogger, private $projectData: IProjectData, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - private $platformService: IPlatformService, + private $platformValidationService: IPlatformValidationService, private $errors: IErrors, private $prompter: IPrompter) { this.$projectData.initializeProjectData(); } public async execute(args: string[]): Promise { - if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { + if (!this.$platformValidationService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); } diff --git a/lib/commands/appstore-upload.ts b/lib/commands/appstore-upload.ts index c1055a2f92..a34019cc93 100644 --- a/lib/commands/appstore-upload.ts +++ b/lib/commands/appstore-upload.ts @@ -1,5 +1,7 @@ import * as path from "path"; import { StringCommandParameter } from "../common/command-params"; +import { BuildController } from "../controllers/build-controller"; +import { IOSBuildData } from "../data/build-data"; export class PublishIOS implements ICommand { public allowedParameters: ICommandParameter[] = [new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector), @@ -13,20 +15,12 @@ export class PublishIOS implements ICommand { private $options: IOptions, private $prompter: IPrompter, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - private $xcodebuildService: IXcodebuildService) { + private $buildController: BuildController, + private $platformValidationService: IPlatformValidationService + ) { this.$projectData.initializeProjectData(); } - private get $platformsData(): IPlatformsData { - return this.$injector.resolve("platformsData"); - } - - // This property was introduced due to the fact that the $platformService dependency - // ultimately tries to resolve the current project's dir and fails if not executed from within a project - private get $platformService(): IPlatformService { - return this.$injector.resolve("platformService"); - } - public async execute(args: string[]): Promise { let username = args[0]; let password = args[1]; @@ -51,50 +45,24 @@ export class PublishIOS implements ICommand { } this.$options.release = true; + const platform = this.$devicePlatformsConstants.iOS.toLowerCase(); if (!ipaFilePath) { - const platform = this.$devicePlatformsConstants.iOS; // No .ipa path provided, build .ipa on out own. - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { - bundle: !!this.$options.bundle, - release: this.$options.release, - useHotModuleReload: false - }; - const platformInfo: IPreparePlatformInfo = { - platform, - appFilesUpdaterOptions, - projectData: this.$projectData, - config: this.$options, - env: this.$options.env - }; - const buildConfig: IBuildConfig = { - projectDir: this.$options.path, - release: this.$options.release, - device: this.$options.device, - provision: this.$options.provision, - teamId: this.$options.teamId, - buildForDevice: true, - iCloudContainerEnvironment: this.$options.iCloudContainerEnvironment, - mobileProvisionIdentifier, - codeSignIdentity - }; - if (mobileProvisionIdentifier || codeSignIdentity) { - this.$logger.info("Building .ipa with the selected mobile provision and/or certificate."); // This is not very correct as if we build multiple targets we will try to sign all of them using the signing identity here. - await this.$platformService.preparePlatform(platformInfo); - await this.$platformService.buildPlatform(platform, buildConfig, this.$projectData); - ipaFilePath = this.$platformService.lastOutputPath(platform, buildConfig, this.$projectData); - } else { - this.$logger.info("No .ipa, mobile provision or certificate set. Perfect! Now we'll build .xcarchive and let Xcode pick the distribution certificate and provisioning profile for you when exporting .ipa for AppStore submission."); - await this.$platformService.preparePlatform(platformInfo); - - const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); + this.$logger.info("Building .ipa with the selected mobile provision and/or certificate."); - const exportPath = await this.$xcodebuildService.buildForAppStore(platformData, this.$projectData, buildConfig); - this.$logger.info("Export at: " + exportPath); + // As we need to build the package for device + this.$options.forDevice = true; - ipaFilePath = exportPath; + const buildData = new IOSBuildData(this.$projectData.projectDir, platform, this.$options); + ipaFilePath = await this.$buildController.prepareAndBuild(buildData); + } else { + this.$logger.info("No .ipa, mobile provision or certificate set. Perfect! Now we'll build .xcarchive and let Xcode pick the distribution certificate and provisioning profile for you when exporting .ipa for AppStore submission."); + const buildData = new IOSBuildData(this.$projectData.projectDir, platform, { ...this.$options, buildForAppStore: true }); + ipaFilePath = await this.$buildController.prepareAndBuild(buildData); + this.$logger.info(`Export at: ${ipaFilePath}`); } } @@ -107,7 +75,7 @@ export class PublishIOS implements ICommand { } public async canExecute(args: string[]): Promise { - if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { + if (!this.$platformValidationService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); } diff --git a/lib/commands/build.ts b/lib/commands/build.ts index 95e709b873..11b02b7e0b 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -5,60 +5,31 @@ export abstract class BuildCommandBase extends ValidatePlatformCommandBase { constructor($options: IOptions, protected $errors: IErrors, $projectData: IProjectData, - $platformsData: IPlatformsData, + $platformsDataService: IPlatformsDataService, protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - $platformService: IPlatformService, + protected $buildController: IBuildController, + $platformValidationService: IPlatformValidationService, private $bundleValidatorHelper: IBundleValidatorHelper, + private $buildDataService: IBuildDataService, protected $logger: ILogger) { - super($options, $platformsData, $platformService, $projectData); + super($options, $platformsDataService, $platformValidationService, $projectData); this.$projectData.initializeProjectData(); } + public dashedOptions = { + watch: { type: OptionType.Boolean, default: false, hasSensitiveValue: false }, + }; + public async executeCore(args: string[]): Promise { const platform = args[0].toLowerCase(); - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { - bundle: !!this.$options.bundle, - release: this.$options.release, - useHotModuleReload: this.$options.hmr - }; - const platformInfo: IPreparePlatformInfo = { - platform, - appFilesUpdaterOptions, - projectData: this.$projectData, - config: this.$options, - env: this.$options.env - }; - - await this.$platformService.preparePlatform(platformInfo); - const buildConfig: IBuildConfig = { - buildForDevice: this.$options.forDevice, - iCloudContainerEnvironment: this.$options.iCloudContainerEnvironment, - projectDir: this.$options.path, - clean: this.$options.clean, - teamId: this.$options.teamId, - device: this.$options.device, - provision: this.$options.provision, - release: this.$options.release, - keyStoreAlias: this.$options.keyStoreAlias, - keyStorePath: this.$options.keyStorePath, - keyStoreAliasPassword: this.$options.keyStoreAliasPassword, - keyStorePassword: this.$options.keyStorePassword, - androidBundle: this.$options.aab - }; - - const outputPath = await this.$platformService.buildPlatform(platform, buildConfig, this.$projectData); - - if (this.$options.copyTo) { - this.$platformService.copyLastOutput(platform, this.$options.copyTo, buildConfig, this.$projectData); - } else { - this.$logger.info(`The build result is located at: ${outputPath}`); - } + const buildData = this.$buildDataService.getBuildData(this.$projectData.projectDir, platform, this.$options); + const outputPath = await this.$buildController.prepareAndBuild(buildData); return outputPath; } protected validatePlatform(platform: string): void { - if (!this.$platformService.isPlatformSupportedForOS(platform, this.$projectData)) { + if (!this.$platformValidationService.isPlatformSupportedForOS(platform, this.$projectData)) { this.$errors.fail(`Applications for platform ${platform} can not be built on this OS`); } @@ -78,7 +49,7 @@ export abstract class BuildCommandBase extends ValidatePlatformCommandBase { return false; } - const result = await this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, platform); + const result = await this.$platformValidationService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, platform); return result; } } @@ -89,16 +60,18 @@ export class BuildIosCommand extends BuildCommandBase implements ICommand { constructor(protected $options: IOptions, $errors: IErrors, $projectData: IProjectData, - $platformsData: IPlatformsData, + $platformsDataService: IPlatformsDataService, $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - $platformService: IPlatformService, + $buildController: IBuildController, + $platformValidationService: IPlatformValidationService, $bundleValidatorHelper: IBundleValidatorHelper, - $logger: ILogger) { - super($options, $errors, $projectData, $platformsData, $devicePlatformsConstants, $platformService, $bundleValidatorHelper, $logger); + $logger: ILogger, + $buildDataService: IBuildDataService) { + super($options, $errors, $projectData, $platformsDataService, $devicePlatformsConstants, $buildController, $platformValidationService, $bundleValidatorHelper, $buildDataService, $logger); } public async execute(args: string[]): Promise { - await this.executeCore([this.$platformsData.availablePlatforms.iOS]); + await this.executeCore([this.$devicePlatformsConstants.iOS.toLowerCase()]); } public async canExecute(args: string[]): Promise { @@ -123,17 +96,19 @@ export class BuildAndroidCommand extends BuildCommandBase implements ICommand { constructor(protected $options: IOptions, protected $errors: IErrors, $projectData: IProjectData, - $platformsData: IPlatformsData, + platformsDataService: IPlatformsDataService, $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - $platformService: IPlatformService, + $buildController: IBuildController, + $platformValidationService: IPlatformValidationService, $bundleValidatorHelper: IBundleValidatorHelper, protected $androidBundleValidatorHelper: IAndroidBundleValidatorHelper, + $buildDataService: IBuildDataService, protected $logger: ILogger) { - super($options, $errors, $projectData, $platformsData, $devicePlatformsConstants, $platformService, $bundleValidatorHelper, $logger); + super($options, $errors, $projectData, platformsDataService, $devicePlatformsConstants, $buildController, $platformValidationService, $bundleValidatorHelper, $buildDataService, $logger); } public async execute(args: string[]): Promise { - await this.executeCore([this.$platformsData.availablePlatforms.Android]); + await this.executeCore([this.$devicePlatformsConstants.Android.toLowerCase()]); if (this.$options.aab) { this.$logger.info(AndroidAppBundleMessages.ANDROID_APP_BUNDLE_DOCS_MESSAGE); diff --git a/lib/commands/clean-app.ts b/lib/commands/clean-app.ts deleted file mode 100644 index b825b7e071..0000000000 --- a/lib/commands/clean-app.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { ValidatePlatformCommandBase } from "./command-base"; - -export class CleanAppCommandBase extends ValidatePlatformCommandBase implements ICommand { - public allowedParameters: ICommandParameter[] = []; - - protected platform: string; - - constructor($options: IOptions, - $projectData: IProjectData, - $platformService: IPlatformService, - protected $errors: IErrors, - protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - $platformsData: IPlatformsData) { - super($options, $platformsData, $platformService, $projectData); - this.$projectData.initializeProjectData(); - } - - public async execute(args: string[]): Promise { - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { - bundle: !!this.$options.bundle, - release: this.$options.release, - useHotModuleReload: false - }; - const platformInfo: IPreparePlatformInfo = { - appFilesUpdaterOptions, - platform: this.platform.toLowerCase(), - config: this.$options, - projectData: this.$projectData, - env: this.$options.env - }; - - return this.$platformService.cleanDestinationApp(platformInfo); - } - - public async canExecute(args: string[]): Promise { - if (!this.$platformService.isPlatformSupportedForOS(this.platform, this.$projectData)) { - this.$errors.fail(`Applications for platform ${this.platform} can not be built on this OS`); - } - - const result = await super.canExecuteCommandBase(this.platform); - return result; - } -} - -export class CleanAppIosCommand extends CleanAppCommandBase implements ICommand { - constructor(protected $options: IOptions, - protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - protected $platformsData: IPlatformsData, - protected $errors: IErrors, - $platformService: IPlatformService, - $projectData: IProjectData) { - super($options, $projectData, $platformService, $errors, $devicePlatformsConstants, $platformsData); - } - - protected get platform(): string { - return this.$devicePlatformsConstants.iOS; - } -} - -$injector.registerCommand("clean-app|ios", CleanAppIosCommand); - -export class CleanAppAndroidCommand extends CleanAppCommandBase implements ICommand { - constructor(protected $options: IOptions, - protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - protected $platformsData: IPlatformsData, - protected $errors: IErrors, - $platformService: IPlatformService, - $projectData: IProjectData) { - super($options, $projectData, $platformService, $errors, $devicePlatformsConstants, $platformsData); - } - - protected get platform(): string { - return this.$devicePlatformsConstants.Android; - } -} - -$injector.registerCommand("clean-app|android", CleanAppAndroidCommand); diff --git a/lib/commands/command-base.ts b/lib/commands/command-base.ts index e4f778336e..7621f92a69 100644 --- a/lib/commands/command-base.ts +++ b/lib/commands/command-base.ts @@ -1,7 +1,7 @@ export abstract class ValidatePlatformCommandBase { constructor(protected $options: IOptions, - protected $platformsData: IPlatformsData, - protected $platformService: IPlatformService, + protected $platformsDataService: IPlatformsDataService, + protected $platformValidationService: IPlatformValidationService, protected $projectData: IProjectData) { } abstract allowedParameters: ICommandParameter[]; @@ -14,7 +14,7 @@ export abstract class ValidatePlatformCommandBase { let result = { canExecute, suppressCommandHelp: !canExecute }; if (canExecute && options.validateOptions) { - const validateOptionsOutput = await this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, platform); + const validateOptionsOutput = await this.$platformValidationService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, platform); result = { canExecute: validateOptionsOutput, suppressCommandHelp: false }; } @@ -22,7 +22,7 @@ export abstract class ValidatePlatformCommandBase { } private async validatePlatformBase(platform: string, notConfiguredEnvOptions: INotConfiguredEnvOptions): Promise { - const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); + const platformData = this.$platformsDataService.getPlatformData(platform, this.$projectData); const platformProjectService = platformData.platformProjectService; const result = await platformProjectService.validate(this.$projectData, this.$options, notConfiguredEnvOptions); return result; diff --git a/lib/commands/debug.ts b/lib/commands/debug.ts index ab62e8ac56..fab91e8499 100644 --- a/lib/commands/debug.ts +++ b/lib/commands/debug.ts @@ -1,6 +1,7 @@ import { cache } from "../common/decorators"; import { ValidatePlatformCommandBase } from "./command-base"; import { LiveSyncCommandHelper } from "../helpers/livesync-command-helper"; +import { DeviceDebugAppService } from "../services/device/device-debug-app-service"; export class DebugPlatformCommand extends ValidatePlatformCommandBase implements ICommand { public allowedParameters: ICommandParameter[] = []; @@ -9,17 +10,17 @@ export class DebugPlatformCommand extends ValidatePlatformCommandBase implements private $bundleValidatorHelper: IBundleValidatorHelper, private $debugService: IDebugService, protected $devicesService: Mobile.IDevicesService, - $platformService: IPlatformService, + $platformValidationService: IPlatformValidationService, $projectData: IProjectData, $options: IOptions, - $platformsData: IPlatformsData, + $platformsDataService: IPlatformsDataService, protected $logger: ILogger, protected $errors: IErrors, private $debugDataService: IDebugDataService, - private $liveSyncService: IDebugLiveSyncService, + private $deviceDebugAppService: DeviceDebugAppService, private $liveSyncCommandHelper: ILiveSyncCommandHelper, private $androidBundleValidatorHelper: IAndroidBundleValidatorHelper) { - super($options, $platformsData, $platformService, $projectData); + super($options, $platformsDataService, $platformValidationService, $projectData); } public async execute(args: string[]): Promise { @@ -41,7 +42,7 @@ export class DebugPlatformCommand extends ValidatePlatformCommandBase implements const debugData = this.$debugDataService.createDebugData(this.$projectData, { device: selectedDeviceForDebug.deviceInfo.identifier }); if (this.$options.start) { - await this.$liveSyncService.printDebugInformation(await this.$debugService.debug(debugData, debugOptions)); + await this.$deviceDebugAppService.printDebugInformation(await this.$debugService.debug(debugData, debugOptions)); return; } @@ -58,7 +59,7 @@ export class DebugPlatformCommand extends ValidatePlatformCommandBase implements public async canExecute(args: string[]): Promise { this.$androidBundleValidatorHelper.validateNoAab(); - if (!this.$platformService.isPlatformSupportedForOS(this.platform, this.$projectData)) { + if (!this.$platformValidationService.isPlatformSupportedForOS(this.platform, this.$projectData)) { this.$errors.fail(`Applications for platform ${this.platform} can not be built on this OS`); } @@ -85,7 +86,7 @@ export class DebugIOSCommand implements ICommand { constructor(protected $errors: IErrors, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - private $platformService: IPlatformService, + private $platformValidationService: IPlatformValidationService, private $options: IOptions, private $injector: IInjector, private $sysInfo: ISysInfo, @@ -108,7 +109,7 @@ export class DebugIOSCommand implements ICommand { } public async canExecute(args: string[]): Promise { - if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { + if (!this.$platformValidationService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); } diff --git a/lib/commands/deploy.ts b/lib/commands/deploy.ts index c83b357ca0..bedfa975be 100644 --- a/lib/commands/deploy.ts +++ b/lib/commands/deploy.ts @@ -1,27 +1,27 @@ import { ANDROID_RELEASE_BUILD_ERROR_MESSAGE } from "../constants"; import { ValidatePlatformCommandBase } from "./command-base"; +import { DeployCommandHelper } from "../helpers/deploy-command-helper"; export class DeployOnDeviceCommand extends ValidatePlatformCommandBase implements ICommand { public allowedParameters: ICommandParameter[] = []; - constructor($platformService: IPlatformService, + constructor($platformValidationService: IPlatformValidationService, private $platformCommandParameter: ICommandParameter, $options: IOptions, $projectData: IProjectData, - private $deployCommandHelper: IDeployCommandHelper, private $errors: IErrors, private $mobileHelper: Mobile.IMobileHelper, - $platformsData: IPlatformsData, + $platformsDataService: IPlatformsDataService, private $bundleValidatorHelper: IBundleValidatorHelper, + private $deployCommandHelper: DeployCommandHelper, private $androidBundleValidatorHelper: IAndroidBundleValidatorHelper) { - super($options, $platformsData, $platformService, $projectData); + super($options, $platformsDataService, $platformValidationService, $projectData); this.$projectData.initializeProjectData(); } public async execute(args: string[]): Promise { - const deployPlatformInfo = this.$deployCommandHelper.getDeployPlatformInfo(args[0]); - - return this.$platformService.deployPlatform(deployPlatformInfo); + const platform = args[0].toLowerCase(); + await this.$deployCommandHelper.deploy(platform); } public async canExecute(args: string[]): Promise { diff --git a/lib/commands/install.ts b/lib/commands/install.ts index 655097d6a8..7e200c4b92 100644 --- a/lib/commands/install.ts +++ b/lib/commands/install.ts @@ -5,8 +5,9 @@ export class InstallCommand implements ICommand { public allowedParameters: ICommandParameter[] = [this.$stringParameter]; constructor(private $options: IOptions, - private $platformsData: IPlatformsData, - private $platformService: IPlatformService, + private $mobileHelper: Mobile.IMobileHelper, + private $platformsDataService: IPlatformsDataService, + private $platformCommandHelper: IPlatformCommandHelper, private $projectData: IProjectData, private $projectDataService: IProjectDataService, private $pluginsService: IPluginsService, @@ -26,15 +27,15 @@ export class InstallCommand implements ICommand { await this.$pluginsService.ensureAllDependenciesAreInstalled(this.$projectData); - for (const platform of this.$platformsData.platformsNames) { - const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); + for (const platform of this.$mobileHelper.platformNames) { + const platformData = this.$platformsDataService.getPlatformData(platform, this.$projectData); const frameworkPackageData = this.$projectDataService.getNSValue(this.$projectData.projectDir, platformData.frameworkPackageName); if (frameworkPackageData && frameworkPackageData.version) { try { const platformProjectService = platformData.platformProjectService; await platformProjectService.validate(this.$projectData, this.$options); - await this.$platformService.addPlatforms([`${platform}@${frameworkPackageData.version}`], this.$projectData, this.$options, this.$options.frameworkPath); + await this.$platformCommandHelper.addPlatforms([`${platform}@${frameworkPackageData.version}`], this.$projectData, this.$options.frameworkPath); } catch (err) { error = `${error}${EOL}${err}`; } diff --git a/lib/commands/list-platforms.ts b/lib/commands/list-platforms.ts index 7210609be8..f21f067ee0 100644 --- a/lib/commands/list-platforms.ts +++ b/lib/commands/list-platforms.ts @@ -3,17 +3,17 @@ import * as helpers from "../common/helpers"; export class ListPlatformsCommand implements ICommand { public allowedParameters: ICommandParameter[] = []; - constructor(private $platformService: IPlatformService, + constructor(private $platformCommandHelper: IPlatformCommandHelper, private $projectData: IProjectData, private $logger: ILogger) { this.$projectData.initializeProjectData(); } public async execute(args: string[]): Promise { - const installedPlatforms = this.$platformService.getInstalledPlatforms(this.$projectData); + const installedPlatforms = this.$platformCommandHelper.getInstalledPlatforms(this.$projectData); if (installedPlatforms.length > 0) { - const preparedPlatforms = this.$platformService.getPreparedPlatforms(this.$projectData); + const preparedPlatforms = this.$platformCommandHelper.getPreparedPlatforms(this.$projectData); if (preparedPlatforms.length > 0) { this.$logger.out("The project is prepared for: ", helpers.formatListOfNames(preparedPlatforms, "and")); } else { @@ -22,7 +22,7 @@ export class ListPlatformsCommand implements ICommand { this.$logger.out("Installed platforms: ", helpers.formatListOfNames(installedPlatforms, "and")); } else { - const formattedPlatformsList = helpers.formatListOfNames(this.$platformService.getAvailablePlatforms(this.$projectData), "and"); + const formattedPlatformsList = helpers.formatListOfNames(this.$platformCommandHelper.getAvailablePlatforms(this.$projectData), "and"); this.$logger.out("Available platforms for this OS: ", formattedPlatformsList); this.$logger.out("No installed platforms found. Use $ tns platform add"); } diff --git a/lib/commands/platform-clean.ts b/lib/commands/platform-clean.ts index 9837902c8e..4d1bb7e39b 100644 --- a/lib/commands/platform-clean.ts +++ b/lib/commands/platform-clean.ts @@ -1,16 +1,19 @@ export class CleanCommand implements ICommand { public allowedParameters: ICommandParameter[] = []; - constructor(private $options: IOptions, - private $projectData: IProjectData, - private $platformService: IPlatformService, + constructor( private $errors: IErrors, - private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements) { + private $options: IOptions, + private $platformCommandHelper: IPlatformCommandHelper, + private $platformValidationService: IPlatformValidationService, + private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, + private $projectData: IProjectData + ) { this.$projectData.initializeProjectData(); } public async execute(args: string[]): Promise { - await this.$platformService.cleanPlatforms(args, this.$projectData, this.$options); + await this.$platformCommandHelper.cleanPlatforms(args, this.$projectData, this.$options.frameworkPath); } public async canExecute(args: string[]): Promise { @@ -19,13 +22,13 @@ export class CleanCommand implements ICommand { } _.each(args, platform => { - this.$platformService.validatePlatform(platform, this.$projectData); + this.$platformValidationService.validatePlatform(platform, this.$projectData); }); for (const platform of args) { - this.$platformService.validatePlatformInstalled(platform, this.$projectData); + this.$platformValidationService.validatePlatformInstalled(platform, this.$projectData); - const currentRuntimeVersion = this.$platformService.getCurrentPlatformVersion(platform, this.$projectData); + const currentRuntimeVersion = this.$platformCommandHelper.getCurrentPlatformVersion(platform, this.$projectData); await this.$platformEnvironmentRequirements.checkEnvironmentRequirements({ platform, projectDir: this.$projectData.projectDir, diff --git a/lib/commands/prepare.ts b/lib/commands/prepare.ts index afa0387614..d047cc1d3a 100644 --- a/lib/commands/prepare.ts +++ b/lib/commands/prepare.ts @@ -1,37 +1,36 @@ import { ValidatePlatformCommandBase } from "./command-base"; +import { PrepareController } from "../controllers/prepare-controller"; +import { PrepareDataService } from "../services/prepare-data-service"; export class PrepareCommand extends ValidatePlatformCommandBase implements ICommand { public allowedParameters = [this.$platformCommandParameter]; + public dashedOptions = { + watch: { type: OptionType.Boolean, default: false, hasSensitiveValue: false }, + }; + constructor($options: IOptions, - $platformService: IPlatformService, + private $prepareController: PrepareController, + $platformValidationService: IPlatformValidationService, $projectData: IProjectData, private $platformCommandParameter: ICommandParameter, - $platformsData: IPlatformsData) { - super($options, $platformsData, $platformService, $projectData); + $platformsDataService: IPlatformsDataService, + private $prepareDataService: PrepareDataService) { + super($options, $platformsDataService, $platformValidationService, $projectData); this.$projectData.initializeProjectData(); } public async execute(args: string[]): Promise { - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { - bundle: !!this.$options.bundle, - release: this.$options.release, - useHotModuleReload: this.$options.hmr - }; - const platformInfo: IPreparePlatformInfo = { - platform: args[0], - appFilesUpdaterOptions, - projectData: this.$projectData, - config: this.$options, - env: this.$options.env - }; - - await this.$platformService.preparePlatform(platformInfo); + const platform = args[0]; + + const prepareData = this.$prepareDataService.getPrepareData(this.$projectData.projectDir, platform, this.$options); + await this.$prepareController.prepare(prepareData); } public async canExecute(args: string[]): Promise { const platform = args[0]; - const result = await this.$platformCommandParameter.validate(platform) && await this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, platform); + const result = await this.$platformCommandParameter.validate(platform) && + await this.$platformValidationService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, platform); if (!result) { return false; } diff --git a/lib/commands/preview.ts b/lib/commands/preview.ts index bb2aab8c35..1194f6e620 100644 --- a/lib/commands/preview.ts +++ b/lib/commands/preview.ts @@ -1,4 +1,5 @@ import { DEVICE_LOG_EVENT_NAME } from "../common/constants"; +import { PreviewAppController } from "../controllers/preview-app-controller"; export class PreviewCommand implements ICommand { public allowedParameters: ICommandParameter[] = []; @@ -7,8 +8,8 @@ export class PreviewCommand implements ICommand { constructor(private $analyticsService: IAnalyticsService, private $bundleValidatorHelper: IBundleValidatorHelper, private $errors: IErrors, - private $liveSyncService: ILiveSyncService, private $logger: ILogger, + private $previewAppController: PreviewAppController, private $networkConnectivityValidator: INetworkConnectivityValidator, private $projectData: IProjectData, private $options: IOptions, @@ -24,10 +25,9 @@ export class PreviewCommand implements ICommand { this.$logger.info(message); }); - await this.$liveSyncService.liveSyncToPreviewApp({ - bundle: !!this.$options.bundle, - useHotModuleReload: this.$options.hmr, + await this.$previewAppController.preview({ projectDir: this.$projectData.projectDir, + useHotModuleReload: this.$options.hmr, env: this.$options.env }); diff --git a/lib/commands/remove-platform.ts b/lib/commands/remove-platform.ts index 3ee9b90dc7..92fbd50f5f 100644 --- a/lib/commands/remove-platform.ts +++ b/lib/commands/remove-platform.ts @@ -1,14 +1,17 @@ export class RemovePlatformCommand implements ICommand { public allowedParameters: ICommandParameter[] = []; - constructor(private $platformService: IPlatformService, - private $projectData: IProjectData, - private $errors: IErrors) { + constructor( + private $errors: IErrors, + private $platformCommandHelper: IPlatformCommandHelper, + private $platformValidationService: IPlatformValidationService, + private $projectData: IProjectData + ) { this.$projectData.initializeProjectData(); } public execute(args: string[]): Promise { - return this.$platformService.removePlatforms(args, this.$projectData); + return this.$platformCommandHelper.removePlatforms(args, this.$projectData); } public async canExecute(args: string[]): Promise { @@ -17,7 +20,7 @@ export class RemovePlatformCommand implements ICommand { } _.each(args, platform => { - this.$platformService.validatePlatform(platform, this.$projectData); + this.$platformValidationService.validatePlatform(platform, this.$projectData); }); return true; diff --git a/lib/commands/run.ts b/lib/commands/run.ts index 2e87e58376..b5465736b7 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -8,12 +8,13 @@ export class RunCommandBase implements ICommand { public platform: string; constructor( private $analyticsService: IAnalyticsService, - private $projectData: IProjectData, + private $androidBundleValidatorHelper: IAndroidBundleValidatorHelper, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $errors: IErrors, private $hostInfo: IHostInfo, private $liveSyncCommandHelper: ILiveSyncCommandHelper, - private $androidBundleValidatorHelper: IAndroidBundleValidatorHelper) { } + private $projectData: IProjectData, + ) { } public allowedParameters: ICommandParameter[] = []; public async execute(args: string[]): Promise { @@ -61,13 +62,14 @@ export class RunIosCommand implements ICommand { return this.$devicePlatformsConstants.iOS; } - constructor(private $platformsData: IPlatformsData, + constructor( private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $errors: IErrors, private $injector: IInjector, - private $platformService: IPlatformService, + private $options: IOptions, + private $platformValidationService: IPlatformValidationService, private $projectDataService: IProjectDataService, - private $options: IOptions) { + ) { } public async execute(args: string[]): Promise { @@ -77,11 +79,11 @@ export class RunIosCommand implements ICommand { public async canExecute(args: string[]): Promise { const projectData = this.$projectDataService.getProjectData(); - if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, projectData)) { + if (!this.$platformValidationService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, projectData)) { this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); } - const result = await this.runCommand.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, projectData, this.$platformsData.availablePlatforms.iOS); + const result = await this.runCommand.canExecute(args) && await this.$platformValidationService.validateOptions(this.$options.provision, this.$options.teamId, projectData, this.$devicePlatformsConstants.iOS.toLowerCase()); return result; } } @@ -102,13 +104,14 @@ export class RunAndroidCommand implements ICommand { return this.$devicePlatformsConstants.Android; } - constructor(private $platformsData: IPlatformsData, + constructor( private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $errors: IErrors, private $injector: IInjector, - private $platformService: IPlatformService, + private $options: IOptions, + private $platformValidationService: IPlatformValidationService, private $projectData: IProjectData, - private $options: IOptions) { } + ) { } public execute(args: string[]): Promise { return this.runCommand.execute(args); @@ -117,7 +120,7 @@ export class RunAndroidCommand implements ICommand { public async canExecute(args: string[]): Promise { await this.runCommand.canExecute(args); - if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.Android, this.$projectData)) { + if (!this.$platformValidationService.isPlatformSupportedForOS(this.$devicePlatformsConstants.Android, this.$projectData)) { this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.Android} can not be built on this OS`); } @@ -125,7 +128,7 @@ export class RunAndroidCommand implements ICommand { this.$errors.fail(ANDROID_RELEASE_BUILD_ERROR_MESSAGE); } - return this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.Android); + return this.$platformValidationService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$devicePlatformsConstants.Android.toLowerCase()); } } diff --git a/lib/commands/test.ts b/lib/commands/test.ts index e96d981793..2d45a126ef 100644 --- a/lib/commands/test.ts +++ b/lib/commands/test.ts @@ -1,8 +1,7 @@ -import * as helpers from "../common/helpers"; +import { LiveSyncCommandHelper } from "../helpers/livesync-command-helper"; abstract class TestCommandBase { public allowedParameters: ICommandParameter[] = []; - private projectFilesConfig: IProjectFilesConfig; protected abstract platform: string; protected abstract $projectData: IProjectData; protected abstract $testExecutionService: ITestExecutionService; @@ -11,16 +10,41 @@ abstract class TestCommandBase { protected abstract $platformEnvironmentRequirements: IPlatformEnvironmentRequirements; protected abstract $errors: IErrors; protected abstract $cleanupService: ICleanupService; + protected abstract $liveSyncCommandHelper: LiveSyncCommandHelper; + protected abstract $devicesService: Mobile.IDevicesService; async execute(args: string[]): Promise { - await this.$testExecutionService.startKarmaServer(this.platform, this.$projectData, this.projectFilesConfig); + let devices = []; + if (this.$options.debugBrk) { + const selectedDeviceForDebug = await this.$devicesService.pickSingleDevice({ + onlyEmulators: this.$options.emulator, + onlyDevices: this.$options.forDevice, + deviceId: this.$options.device + }); + devices = [selectedDeviceForDebug]; + // const debugData = this.getDebugData(platform, projectData, deployOptions, { device: selectedDeviceForDebug.deviceInfo.identifier }); + // await this.$debugService.debug(debugData, this.$options); + } else { + devices = await this.$liveSyncCommandHelper.getDeviceInstances(this.platform); + } + + if (!this.$options.env) { this.$options.env = { }; } + this.$options.env.unitTesting = true; + + const liveSyncInfo = this.$liveSyncCommandHelper.createLiveSyncInfo(); + + const deviceDebugMap: IDictionary = {}; + devices.forEach(device => deviceDebugMap[device.deviceInfo.identifier] = this.$options.debugBrk); + + const deviceDescriptors = await this.$liveSyncCommandHelper.createDeviceDescriptors(devices, this.platform, { deviceDebugMap }); + + await this.$testExecutionService.startKarmaServer(this.platform, liveSyncInfo, deviceDescriptors); } async canExecute(args: string[]): Promise { this.$projectData.initializeProjectData(); this.$analyticsService.setShouldDispose(this.$options.justlaunch || !this.$options.watch); this.$cleanupService.setShouldDispose(this.$options.justlaunch || !this.$options.watch); - this.projectFilesConfig = helpers.getProjectFilesConfig({ isReleaseBuild: this.$options.release }); const output = await this.$platformEnvironmentRequirements.checkEnvironmentRequirements({ platform: this.platform, @@ -54,7 +78,9 @@ class TestAndroidCommand extends TestCommandBase implements ICommand { protected $options: IOptions, protected $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, protected $errors: IErrors, - protected $cleanupService: ICleanupService) { + protected $cleanupService: ICleanupService, + protected $liveSyncCommandHelper: LiveSyncCommandHelper, + protected $devicesService: Mobile.IDevicesService) { super(); } @@ -69,7 +95,9 @@ class TestIosCommand extends TestCommandBase implements ICommand { protected $options: IOptions, protected $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, protected $errors: IErrors, - protected $cleanupService: ICleanupService) { + protected $cleanupService: ICleanupService, + protected $liveSyncCommandHelper: LiveSyncCommandHelper, + protected $devicesService: Mobile.IDevicesService) { super(); } diff --git a/lib/commands/update-platform.ts b/lib/commands/update-platform.ts index 61d62f779c..b6a6f98f8d 100644 --- a/lib/commands/update-platform.ts +++ b/lib/commands/update-platform.ts @@ -1,16 +1,19 @@ export class UpdatePlatformCommand implements ICommand { public allowedParameters: ICommandParameter[] = []; - constructor(private $options: IOptions, - private $projectData: IProjectData, - private $platformService: IPlatformService, + constructor( + private $errors: IErrors, + private $options: IOptions, private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, - private $errors: IErrors) { + private $platformCommandHelper: IPlatformCommandHelper, + private $platformValidationService: IPlatformValidationService, + private $projectData: IProjectData, + ) { this.$projectData.initializeProjectData(); } public async execute(args: string[]): Promise { - await this.$platformService.updatePlatforms(args, this.$projectData, this.$options); + await this.$platformCommandHelper.updatePlatforms(args, this.$projectData); } public async canExecute(args: string[]): Promise { @@ -20,7 +23,7 @@ export class UpdatePlatformCommand implements ICommand { _.each(args, arg => { const platform = arg.split("@")[0]; - this.$platformService.validatePlatform(platform, this.$projectData); + this.$platformValidationService.validatePlatform(platform, this.$projectData); }); for (const arg of args) { diff --git a/lib/commands/update.ts b/lib/commands/update.ts index afbd5f7c0a..50b3c00d7b 100644 --- a/lib/commands/update.ts +++ b/lib/commands/update.ts @@ -5,15 +5,18 @@ import { ValidatePlatformCommandBase } from "./command-base"; export class UpdateCommand extends ValidatePlatformCommandBase implements ICommand { public allowedParameters: ICommandParameter[] = []; - constructor($options: IOptions, - $projectData: IProjectData, - $platformService: IPlatformService, - $platformsData: IPlatformsData, + constructor( + private $fs: IFileSystem, + private $logger: ILogger, + $options: IOptions, + private $platformCommandHelper: IPlatformCommandHelper, + $platformsDataService: IPlatformsDataService, + $platformValidationService: IPlatformValidationService, private $pluginsService: IPluginsService, + $projectData: IProjectData, private $projectDataService: IProjectDataService, - private $fs: IFileSystem, - private $logger: ILogger) { - super($options, $platformsData, $platformService, $projectData); + ) { + super($options, $platformsDataService, $platformValidationService, $projectData); this.$projectData.initializeProjectData(); } @@ -78,11 +81,11 @@ export class UpdateCommand extends ValidatePlatformCommandBase implements IComma const platforms = this.getPlatforms(); for (const platform of _.xor(platforms.installed, platforms.packagePlatforms)) { - const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); + const platformData = this.$platformsDataService.getPlatformData(platform, this.$projectData); this.$projectDataService.removeNSProperty(this.$projectData.projectDir, platformData.frameworkPackageName); } - await this.$platformService.removePlatforms(platforms.installed, this.$projectData); + await this.$platformCommandHelper.removePlatforms(platforms.installed, this.$projectData); await this.$pluginsService.remove(constants.TNS_CORE_MODULES_NAME, this.$projectData); if (!!this.$projectData.dependencies[constants.TNS_CORE_MODULES_WIDGETS_NAME]) { await this.$pluginsService.remove(constants.TNS_CORE_MODULES_WIDGETS_NAME, this.$projectData); @@ -94,12 +97,12 @@ export class UpdateCommand extends ValidatePlatformCommandBase implements IComma if (args.length === 1) { for (const platform of platforms.packagePlatforms) { - await this.$platformService.addPlatforms([platform + "@" + args[0]], this.$projectData, this.$options, this.$options.frameworkPath); + await this.$platformCommandHelper.addPlatforms([platform + "@" + args[0]], this.$projectData, this.$options.frameworkPath); } await this.$pluginsService.add(`${constants.TNS_CORE_MODULES_NAME}@${args[0]}`, this.$projectData); } else { - await this.$platformService.addPlatforms(platforms.packagePlatforms, this.$projectData, this.$options, this.$options.frameworkPath); + await this.$platformCommandHelper.addPlatforms(platforms.packagePlatforms, this.$projectData, this.$options.frameworkPath); await this.$pluginsService.add(constants.TNS_CORE_MODULES_NAME, this.$projectData); } @@ -107,12 +110,12 @@ export class UpdateCommand extends ValidatePlatformCommandBase implements IComma } private getPlatforms(): { installed: string[], packagePlatforms: string[] } { - const installedPlatforms = this.$platformService.getInstalledPlatforms(this.$projectData); - const availablePlatforms = this.$platformService.getAvailablePlatforms(this.$projectData); + const installedPlatforms = this.$platformCommandHelper.getInstalledPlatforms(this.$projectData); + const availablePlatforms = this.$platformCommandHelper.getAvailablePlatforms(this.$projectData); const packagePlatforms: string[] = []; for (const platform of availablePlatforms) { - const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); + const platformData = this.$platformsDataService.getPlatformData(platform, this.$projectData); const platformVersion = this.$projectDataService.getNSValue(this.$projectData.projectDir, platformData.frameworkPackageName); if (platformVersion) { packagePlatforms.push(platform); diff --git a/lib/common/declarations.d.ts b/lib/common/declarations.d.ts index 5b0e0314a9..fb97c9c52c 100644 --- a/lib/common/declarations.d.ts +++ b/lib/common/declarations.d.ts @@ -1507,6 +1507,7 @@ interface IPromiseActions { interface IDeferPromise extends IPromiseActions { isRejected(): boolean; isPending(): boolean; + getResult(): any; promise: Promise; } diff --git a/lib/common/definitions/mobile.d.ts b/lib/common/definitions/mobile.d.ts index 1ce531beef..6997689d80 100644 --- a/lib/common/definitions/mobile.d.ts +++ b/lib/common/definitions/mobile.d.ts @@ -495,7 +495,9 @@ declare module Mobile { * Returns a single device based on the specified options. If more than one devices are matching, * prompts the user for a manual choice or returns the first one for non interactive terminals. */ - pickSingleDevice(options: IPickSingleDeviceOptions): Promise + pickSingleDevice(options: IPickSingleDeviceOptions): Promise; + + getPlatformsFromDeviceDescriptors(deviceDescriptors: ILiveSyncDeviceInfo[]): string[]; } interface IPickSingleDeviceOptions { @@ -897,16 +899,6 @@ declare module Mobile { connectToPort(connectToPortData: IConnectToPortData): Promise; } - interface IEmulatorSettingsService { - /** - * Gives information if current project can be started in emulator. - * @param {string} platform The mobile platform of the emulator (android, ios, wp8). - * @returns {boolean} true in case the project can be started in emulator. In case not, the method will throw error. - */ - canStart(platform: string): boolean; - minVersion: number; - } - interface IRunApplicationOnEmulatorOptions { /** * The identifier of the application that will be started on device. @@ -993,6 +985,7 @@ declare module Mobile { buildDevicePath(...args: string[]): string; correctDevicePath(filePath: string): string; isiOSTablet(deviceName: string): boolean; + getDeviceFileContent(device: Mobile.IDevice, deviceFilePath: string, projectData: IProjectData): Promise; } interface IEmulatorHelper { diff --git a/lib/common/helpers.ts b/lib/common/helpers.ts index fd5b6044e4..82860f25a0 100644 --- a/lib/common/helpers.ts +++ b/lib/common/helpers.ts @@ -130,10 +130,12 @@ export function deferPromise(): IDeferPromise { let isResolved = false; let isRejected = false; let promise: Promise; + let result: T | PromiseLike; promise = new Promise((innerResolve, innerReject) => { resolve = (value?: T | PromiseLike) => { isResolved = true; + result = value; return innerResolve(value); }; @@ -151,7 +153,8 @@ export function deferPromise(): IDeferPromise { reject, isResolved: () => isResolved, isRejected: () => isRejected, - isPending: () => !isResolved && !isRejected + isPending: () => !isResolved && !isRejected, + getResult: () => result }; } diff --git a/lib/common/mobile/mobile-core/devices-service.ts b/lib/common/mobile/mobile-core/devices-service.ts index 72ebf1b517..d71df7cd0a 100644 --- a/lib/common/mobile/mobile-core/devices-service.ts +++ b/lib/common/mobile/mobile-core/devices-service.ts @@ -601,6 +601,16 @@ export class DevicesService extends EventEmitter implements Mobile.IDevicesServi } } + public getPlatformsFromDeviceDescriptors(deviceDescriptors: ILiveSyncDeviceInfo[]): string[] { + const platforms = _(deviceDescriptors) + .map(device => this.getDeviceByIdentifier(device.identifier)) + .map(device => device.deviceInfo.platform.toLowerCase()) + .uniq() + .value(); + + return platforms; + } + private async initializeCore(deviceInitOpts?: Mobile.IDevicesServicesInitializationOptions): Promise { if (this._isInitialized) { return; diff --git a/lib/common/mobile/mobile-helper.ts b/lib/common/mobile/mobile-helper.ts index 18ab7b6341..3d2ff846cf 100644 --- a/lib/common/mobile/mobile-helper.ts +++ b/lib/common/mobile/mobile-helper.ts @@ -1,9 +1,12 @@ import * as helpers from "../helpers"; +import * as shell from "shelljs"; +import * as temp from "temp"; export class MobileHelper implements Mobile.IMobileHelper { private static DEVICE_PATH_SEPARATOR = "/"; constructor(private $errors: IErrors, + private $fs: IFileSystem, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants) { } public get platformNames(): string[] { @@ -58,5 +61,24 @@ export class MobileHelper implements Mobile.IMobileHelper { public isiOSTablet(deviceName: string): boolean { return deviceName && deviceName.toLowerCase().indexOf("ipad") !== -1; } + + public async getDeviceFileContent(device: Mobile.IDevice, deviceFilePath: string, projectData: IProjectData): Promise { + temp.track(); + const uniqueFilePath = temp.path({ suffix: ".tmp" }); + const platform = device.deviceInfo.platform.toLowerCase(); + try { + await device.fileSystem.getFile(deviceFilePath, projectData.projectIdentifiers[platform], uniqueFilePath); + } catch (e) { + return null; + } + + if (this.$fs.exists(uniqueFilePath)) { + const text = this.$fs.readText(uniqueFilePath); + shell.rm(uniqueFilePath); + return text; + } + + return null; + } } $injector.register("mobileHelper", MobileHelper); diff --git a/lib/common/services/hooks-service.ts b/lib/common/services/hooks-service.ts index e1bd095e64..8822da5d3a 100644 --- a/lib/common/services/hooks-service.ts +++ b/lib/common/services/hooks-service.ts @@ -84,7 +84,7 @@ export class HooksService implements IHooksService { results.push(await this.executeHooksInDirectory(hooksDirectory, hookName, hookArguments)); } } catch (err) { - this.$logger.trace("Failed during hook execution."); + this.$logger.trace(`Failed during hook execution ${hookName}.`); this.$errors.failWithoutHelp(err.message || err); } diff --git a/lib/common/services/livesync/sync-batch.ts b/lib/common/services/livesync/sync-batch.ts deleted file mode 100644 index 867c4b01bf..0000000000 --- a/lib/common/services/livesync/sync-batch.ts +++ /dev/null @@ -1,57 +0,0 @@ -// https://github.com/Microsoft/TypeScript/blob/master/src/compiler/tsc.ts#L487-L489 -export const SYNC_WAIT_THRESHOLD = 250; //milliseconds - -export class SyncBatch { - private timer: NodeJS.Timer = null; - private syncQueue: string[] = []; - private syncInProgress: boolean = false; - - constructor(private $logger: ILogger, - private $projectFilesManager: IProjectFilesManager, - private done: () => Promise) { } - - private get filesToSync(): string[] { - const filteredFiles = _.remove(this.syncQueue, syncFile => this.$projectFilesManager.isFileExcluded(syncFile)); - this.$logger.trace("Removed files from syncQueue: ", filteredFiles); - return this.syncQueue; - } - - public get syncPending(): boolean { - return this.syncQueue.length > 0; - } - - public async syncFiles(syncAction: (filesToSync: string[]) => Promise): Promise { - if (this.filesToSync.length > 0) { - await syncAction(this.filesToSync); - this.reset(); - } - } - - public async addFile(file: string): Promise { - if (this.timer) { - clearTimeout(this.timer); - this.timer = null; - } - - this.syncQueue.push(file); - - if (!this.syncInProgress) { - this.timer = setTimeout(async () => { - if (this.syncQueue.length > 0) { - this.$logger.trace("Syncing %s", this.syncQueue.join(", ")); - try { - this.syncInProgress = true; - await this.done(); - } finally { - this.syncInProgress = false; - } - } - this.timer = null; - }, SYNC_WAIT_THRESHOLD); - } - } - - private reset(): void { - this.syncQueue = []; - } -} diff --git a/lib/common/test/unit-tests/services/net-service.ts b/lib/common/test/unit-tests/services/net-service.ts index 38b0ff434b..daee64ed79 100644 --- a/lib/common/test/unit-tests/services/net-service.ts +++ b/lib/common/test/unit-tests/services/net-service.ts @@ -27,7 +27,7 @@ describe("net", () => { const childProcess = testInjector.resolve("childProcess"); childProcess.exec = async (command: string, options?: any, execOptions?: IExecOptions): Promise => { - const platformsData: IDictionary = { + const platformsDataService: IDictionary = { linux: { data: `Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State @@ -67,10 +67,10 @@ Active Connections execCalledCount++; - let data = platformsData[platform].data; + let data = platformsDataService[platform].data; if (port) { - data += `${EOL}${platformsData[platform].portData}`; + data += `${EOL}${platformsDataService[platform].portData}`; } if (iteration) { diff --git a/lib/common/yok.ts b/lib/common/yok.ts index ef64c199b3..556e267c7f 100644 --- a/lib/common/yok.ts +++ b/lib/common/yok.ts @@ -6,11 +6,11 @@ import { CommandsDelimiters } from "./constants"; let indent = ""; function trace(formatStr: string, ...args: any[]) { // uncomment following lines when debugging dependency injection - // var args = []; - // for (var _i = 1; _i < arguments.length; _i++) { - // args[_i - 1] = arguments[_i]; + // const items: any[] = []; + // for (let _i = 1; _i < arguments.length; _i++) { + // items[_i - 1] = arguments[_i]; // } - // var util = require("util"); + // const util = require("util"); // console.log(util.format.apply(util, [indent + formatStr].concat(args))); } diff --git a/lib/constants.ts b/lib/constants.ts index ccd1ae5053..65280f1197 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -139,6 +139,11 @@ export const POST_INSTALL_COMMAND_NAME = "post-install-cli"; export const ANDROID_RELEASE_BUILD_ERROR_MESSAGE = "When producing a release build, you need to specify all --key-store-* options."; export const CACACHE_DIRECTORY_NAME = "_cacache"; +export const FILES_CHANGE_EVENT_NAME = "filesChangeEvent"; +export const INITIAL_SYNC_EVENT_NAME = "initialSyncEvent"; +export const PREPARE_READY_EVENT_NAME = "prepareReadyEvent"; +export const WEBPACK_COMPILATION_COMPLETE = "webpackCompilationComplete"; + export class DebugCommandErrors { public static UNABLE_TO_USE_FOR_DEVICE_AND_EMULATOR = "The options --for-device and --emulator cannot be used simultaneously. Please use only one of them."; public static NO_DEVICES_EMULATORS_FOUND_FOR_OPTIONS = "Unable to find device or emulator for specified options."; @@ -274,12 +279,12 @@ export class AndroidAppBundleMessages { public static ANDROID_APP_BUNDLE_PUBLISH_DOCS_MESSAGE = "How to use Android App Bundle for publishing: https://docs.nativescript.org/tooling/publishing/publishing-android-apps#android-app-bundle"; } -export const LiveSyncEvents = { - liveSyncStopped: "liveSyncStopped", +export const RunOnDeviceEvents = { + runOnDeviceStopped: "runOnDeviceStopped", // In case we name it error, EventEmitter expects instance of Error to be raised and will also raise uncaught exception in case there's no handler - liveSyncError: "liveSyncError", + runOnDeviceError: "runOnDeviceError", previewAppLiveSyncError: PreviewAppLiveSyncEvents.PREVIEW_APP_LIVE_SYNC_ERROR, - liveSyncExecuted: "liveSyncExecuted", - liveSyncStarted: "liveSyncStarted", - liveSyncNotification: "notify" + runOnDeviceExecuted: "runOnDeviceExecuted", + runOnDeviceStarted: "runOnDeviceStarted", + runOnDeviceNotification: "notify" }; diff --git a/lib/controllers/build-controller.ts b/lib/controllers/build-controller.ts new file mode 100644 index 0000000000..a0acfd296c --- /dev/null +++ b/lib/controllers/build-controller.ts @@ -0,0 +1,127 @@ +import * as constants from "../constants"; +import { Configurations } from "../common/constants"; +import { EventEmitter } from "events"; +import { attachAwaitDetach } from "../common/helpers"; + +export class BuildController extends EventEmitter implements IBuildController { + constructor( + private $analyticsService: IAnalyticsService, + private $buildArtefactsService: IBuildArtefactsService, + private $buildInfoFileService: IBuildInfoFileService, + private $fs: IFileSystem, + private $logger: ILogger, + private $injector: IInjector, + private $mobileHelper: Mobile.IMobileHelper, + private $projectDataService: IProjectDataService, + private $projectChangesService: IProjectChangesService, + private $prepareController: IPrepareController, + ) { super(); } + + private get $platformsDataService(): IPlatformsDataService { + return this.$injector.resolve("platformsDataService"); + } + + public async prepareAndBuild(buildData: IBuildData): Promise { + await this.$prepareController.prepare(buildData); + const result = await this.build(buildData); + + return result; + } + + public async build(buildData: IBuildData): Promise { + this.$logger.out("Building project..."); + + const platform = buildData.platform.toLowerCase(); + const projectData = this.$projectDataService.getProjectData(buildData.projectDir); + const platformData = this.$platformsDataService.getPlatformData(platform, projectData); + + const action = constants.TrackActionNames.Build; + const isForDevice = this.$mobileHelper.isAndroidPlatform(platform) ? null : buildData && buildData.buildForDevice; + + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action, + isForDevice, + platform, + projectDir: projectData.projectDir, + additionalData: `${buildData.release ? Configurations.Release : Configurations.Debug}_${buildData.clean ? constants.BuildStates.Clean : constants.BuildStates.Incremental}` + }); + + if (buildData.clean) { + await platformData.platformProjectService.cleanProject(platformData.projectRoot, projectData); + } + + const handler = (data: any) => { + this.emit(constants.BUILD_OUTPUT_EVENT_NAME, data); + this.$logger.printInfoMessageOnSameLine(data.data.toString()); + }; + + await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME, platformData.platformProjectService, handler, platformData.platformProjectService.buildProject(platformData.projectRoot, projectData, buildData)); + + const buildInfoFileDir = platformData.getBuildOutputPath(buildData); + this.$buildInfoFileService.saveBuildInfoFile(platformData, buildInfoFileDir); + + this.$logger.out("Project successfully built."); + + const result = await this.$buildArtefactsService.getLatestApplicationPackagePath(platformData, buildData); + + if (buildData.copyTo) { + this.$buildArtefactsService.copyLastOutput(buildData.copyTo, platformData, buildData); + this.$logger.info(`The build result is located at: ${buildInfoFileDir}`); + } + + return result; + } + + public async buildIfNeeded(buildData: IBuildData): Promise { + let result = null; + + const shouldBuildPlatform = await this.shouldBuild(buildData); + if (shouldBuildPlatform) { + result = await this.build(buildData); + } + + return result; + } + + public async shouldBuild(buildData: IBuildData): Promise { + const projectData = this.$projectDataService.getProjectData(buildData.projectDir); + const platformData = this.$platformsDataService.getPlatformData(buildData.platform, projectData); + const outputPath = buildData.outputPath || platformData.getBuildOutputPath(buildData); + + if (buildData.release && this.$projectChangesService.currentChanges.hasChanges) { + return true; + } + + const changesInfo = this.$projectChangesService.currentChanges || await this.$projectChangesService.checkForChanges(platformData, projectData, buildData); + if (changesInfo.changesRequireBuild) { + return true; + } + + if (!this.$fs.exists(outputPath)) { + return true; + } + + const validBuildOutputData = platformData.getValidBuildOutputData(buildData); + const packages = this.$buildArtefactsService.getAllApplicationPackages(outputPath, validBuildOutputData); + if (packages.length === 0) { + return true; + } + + const prepareInfo = this.$projectChangesService.getPrepareInfo(platformData); + const buildInfo = this.$buildInfoFileService.getBuildInfoFromFile(platformData, buildData); + if (!prepareInfo || !buildInfo) { + return true; + } + + if (buildData.clean) { + return true; + } + + if (prepareInfo.time === buildInfo.prepareTime) { + return false; + } + + return prepareInfo.changesRequireBuildTime !== buildInfo.prepareTime; + } +} +$injector.register("buildController", BuildController); diff --git a/lib/controllers/deploy-controller.ts b/lib/controllers/deploy-controller.ts new file mode 100644 index 0000000000..5f43a8d08f --- /dev/null +++ b/lib/controllers/deploy-controller.ts @@ -0,0 +1,22 @@ +export class DeployController { + + constructor( + private $buildDataService: IBuildDataService, + private $buildController: IBuildController, + private $deviceInstallAppService: IDeviceInstallAppService, + private $devicesService: Mobile.IDevicesService + ) { } + + public async deploy(data: IRunData): Promise { + const { projectDir, liveSyncInfo, deviceDescriptors } = data; + + const executeAction = async (device: Mobile.IDevice) => { + const buildData = this.$buildDataService.getBuildData(projectDir, device.deviceInfo.platform, liveSyncInfo); + await this.$buildController.prepareAndBuild(buildData); + await this.$deviceInstallAppService.installOnDevice(device, buildData); + }; + + await this.$devicesService.execute(executeAction, (device: Mobile.IDevice) => _.some(deviceDescriptors, deviceDescriptor => deviceDescriptor.identifier === device.deviceInfo.identifier)); + } +} +$injector.register("deployController", DeployController); diff --git a/lib/controllers/platform-controller.ts b/lib/controllers/platform-controller.ts new file mode 100644 index 0000000000..fb7142d109 --- /dev/null +++ b/lib/controllers/platform-controller.ts @@ -0,0 +1,78 @@ +import { NativePlatformStatus } from "../constants"; +import * as path from "path"; + +export class PlatformController implements IPlatformController { + constructor( + private $addPlatformService: IAddPlatformService, + private $errors: IErrors, + private $fs: IFileSystem, + private $logger: ILogger, + private $packageInstallationManager: IPackageInstallationManager, + private $projectDataService: IProjectDataService, + private $platformsDataService: IPlatformsDataService, + private $projectChangesService: IProjectChangesService, + ) { } + + public async addPlatform(addPlatformData: IAddPlatformData): Promise { + const [ platform, version ] = addPlatformData.platform.toLowerCase().split("@"); + const projectData = this.$projectDataService.getProjectData(addPlatformData.projectDir); + const platformData = this.$platformsDataService.getPlatformData(platform, projectData); + + this.$logger.trace(`Creating NativeScript project for the ${platform} platform`); + this.$logger.trace(`Path: ${platformData.projectRoot}`); + this.$logger.trace(`Package: ${projectData.projectIdentifiers[platform]}`); + this.$logger.trace(`Name: ${projectData.projectName}`); + + this.$logger.out("Copying template files..."); + + const packageToInstall = await this.getPackageToInstall(platformData, projectData, addPlatformData.frameworkPath, version); + + const installedPlatformVersion = await this.$addPlatformService.addPlatformSafe(projectData, platformData, packageToInstall, addPlatformData.nativePrepare); + + this.$fs.ensureDirectoryExists(path.join(projectData.platformsDir, platform)); + this.$logger.out(`Platform ${platform} successfully added. v${installedPlatformVersion}`); + } + + public async addPlatformIfNeeded(addPlatformData: IAddPlatformData): Promise { + const [ platform ] = addPlatformData.platform.toLowerCase().split("@"); + const projectData = this.$projectDataService.getProjectData(addPlatformData.projectDir); + const platformData = this.$platformsDataService.getPlatformData(platform, projectData); + + const shouldAddPlatform = this.shouldAddPlatform(platformData, projectData, addPlatformData.nativePrepare); + if (shouldAddPlatform) { + await this.addPlatform(addPlatformData); + } + } + + private async getPackageToInstall(platformData: IPlatformData, projectData: IProjectData, frameworkPath?: string, version?: string): Promise { + let result = null; + if (frameworkPath) { + if (!this.$fs.exists(frameworkPath)) { + this.$errors.fail(`Invalid frameworkPath: ${frameworkPath}. Please ensure the specified frameworkPath exists.`); + } + result = path.resolve(frameworkPath); + } else { + if (!version) { + const currentPlatformData = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName); + version = (currentPlatformData && currentPlatformData.version) || + await this.$packageInstallationManager.getLatestCompatibleVersion(platformData.frameworkPackageName); + } + + result = `${platformData.frameworkPackageName}@${version}`; + } + + return result; + } + + private shouldAddPlatform(platformData: IPlatformData, projectData: IProjectData, nativePrepare: INativePrepare): boolean { + const platformName = platformData.platformNameLowerCase; + const hasPlatformDirectory = this.$fs.exists(path.join(projectData.platformsDir, platformName)); + const shouldAddNativePlatform = !nativePrepare || !nativePrepare.skipNativePrepare; + const prepareInfo = this.$projectChangesService.getPrepareInfo(platformData); + const requiresNativePlatformAdd = prepareInfo && prepareInfo.nativePlatformStatus === NativePlatformStatus.requiresPlatformAdd; + const result = !hasPlatformDirectory || (shouldAddNativePlatform && requiresNativePlatformAdd); + + return !!result; + } +} +$injector.register("platformController", PlatformController); diff --git a/lib/controllers/prepare-controller.ts b/lib/controllers/prepare-controller.ts new file mode 100644 index 0000000000..aea42ebccb --- /dev/null +++ b/lib/controllers/prepare-controller.ts @@ -0,0 +1,151 @@ +import * as child_process from "child_process"; +import * as choki from "chokidar"; +import { hook } from "../common/helpers"; +import { performanceLog } from "../common/decorators"; +import { EventEmitter } from "events"; +import * as path from "path"; +import { PREPARE_READY_EVENT_NAME, WEBPACK_COMPILATION_COMPLETE } from "../constants"; + +interface IPlatformWatcherData { + webpackCompilerProcess: child_process.ChildProcess; + nativeFilesWatcher: choki.FSWatcher; +} + +export class PrepareController extends EventEmitter { + private watchersData: IDictionary> = {}; + private isInitialPrepareReady = false; + private persistedData: IFilesChangeEventData[] = []; + + constructor( + private $platformController: IPlatformController, + public $hooksService: IHooksService, + private $logger: ILogger, + private $platformsDataService: IPlatformsDataService, + private $prepareNativePlatformService: IPrepareNativePlatformService, + private $projectChangesService: IProjectChangesService, + private $projectDataService: IProjectDataService, + private $webpackCompilerService: IWebpackCompilerService + ) { super(); } + + @performanceLog() + @hook("prepare") + public async prepare(prepareData: IPrepareData): Promise { + await this.$platformController.addPlatformIfNeeded(prepareData); + + this.$logger.out("Preparing project..."); + let result = null; + + const projectData = this.$projectDataService.getProjectData(prepareData.projectDir); + const platformData = this.$platformsDataService.getPlatformData(prepareData.platform, projectData); + + if (prepareData.watch) { + result = await this.startWatchersWithPrepare(platformData, projectData, prepareData); + } else { + await this.$webpackCompilerService.compileWithoutWatch(platformData, projectData, { watch: false, env: prepareData.env }); + await this.$prepareNativePlatformService.prepareNativePlatform(platformData, projectData, prepareData); + } + + this.$projectChangesService.savePrepareInfo(platformData); + + this.$logger.out(`Project successfully prepared (${prepareData.platform.toLowerCase()})`); + + return result; + } + + public stopWatchers(projectDir: string, platform: string): void { + const platformLowerCase = platform.toLowerCase(); + + if (this.watchersData && this.watchersData[projectDir] && this.watchersData[projectDir][platformLowerCase] && this.watchersData[projectDir][platformLowerCase].nativeFilesWatcher) { + this.watchersData[projectDir][platformLowerCase].nativeFilesWatcher.close(); + this.watchersData[projectDir][platformLowerCase].nativeFilesWatcher = null; + } + + if (this.watchersData && this.watchersData[projectDir] && this.watchersData[projectDir][platformLowerCase] && this.watchersData[projectDir][platformLowerCase].webpackCompilerProcess) { + this.$webpackCompilerService.stopWebpackCompiler(platform); + this.watchersData[projectDir][platformLowerCase].webpackCompilerProcess = null; + } + } + + @hook("watch") + private async startWatchersWithPrepare(platformData: IPlatformData, projectData: IProjectData, prepareData: IPrepareData): Promise { + if (!this.watchersData[projectData.projectDir]) { + this.watchersData[projectData.projectDir] = {}; + } + + if (!this.watchersData[projectData.projectDir][platformData.platformNameLowerCase]) { + this.watchersData[projectData.projectDir][platformData.platformNameLowerCase] = { + nativeFilesWatcher: null, + webpackCompilerProcess: null + }; + } + + await this.startJSWatcherWithPrepare(platformData, projectData, { env: prepareData.env }); // -> start watcher + initial compilation + const hasNativeChanges = await this.startNativeWatcherWithPrepare(platformData, projectData, prepareData); // -> start watcher + initial prepare + + const result = { platform: platformData.platformNameLowerCase, hasNativeChanges }; + const hasPersistedDataWithNativeChanges = this.persistedData.find(data => data.platform === result.platform && data.hasNativeChanges); + if (hasPersistedDataWithNativeChanges) { + result.hasNativeChanges = true; + } + + this.isInitialPrepareReady = true; + + if (this.persistedData && this.persistedData.length) { + this.emitPrepareEvent({ files: [], hasNativeChanges: result.hasNativeChanges, hmrData: null, platform: platformData.platformNameLowerCase }); + } + + return result; + } + + private async startJSWatcherWithPrepare(platformData: IPlatformData, projectData: IProjectData, config: IWebpackCompilerConfig): Promise { + if (!this.watchersData[projectData.projectDir][platformData.platformNameLowerCase].webpackCompilerProcess) { + this.$webpackCompilerService.on(WEBPACK_COMPILATION_COMPLETE, data => { + this.emitPrepareEvent({ ...data, hasNativeChanges: false, platform: platformData.platformNameLowerCase }); + }); + + const childProcess = await this.$webpackCompilerService.compileWithWatch(platformData, projectData, config); + this.watchersData[projectData.projectDir][platformData.platformNameLowerCase].webpackCompilerProcess = childProcess; + } + } + + private async startNativeWatcherWithPrepare(platformData: IPlatformData, projectData: IProjectData, prepareData: IPrepareData): Promise { + if ((prepareData.nativePrepare && prepareData.nativePrepare.skipNativePrepare) || this.watchersData[projectData.projectDir][platformData.platformNameLowerCase].nativeFilesWatcher) { + return false; + } + + const patterns = [ + path.join(projectData.getAppResourcesRelativeDirectoryPath(), platformData.normalizedPlatformName), + `node_modules/**/platforms/${platformData.platformNameLowerCase}/` + ]; + const watcherOptions: choki.WatchOptions = { + ignoreInitial: true, + cwd: projectData.projectDir, + awaitWriteFinish: { + pollInterval: 100, + stabilityThreshold: 500 + }, + ignored: ["**/.*", ".*"] // hidden files + }; + const watcher = choki.watch(patterns, watcherOptions) + .on("all", async (event: string, filePath: string) => { + filePath = path.join(projectData.projectDir, filePath); + this.$logger.info(`Chokidar raised event ${event} for ${filePath}.`); + this.emitPrepareEvent({ files: [], hmrData: null, hasNativeChanges: true, platform: platformData.platformNameLowerCase }); + }); + + this.watchersData[projectData.projectDir][platformData.platformNameLowerCase].nativeFilesWatcher = watcher; + + const hasNativeChanges = await this.$prepareNativePlatformService.prepareNativePlatform(platformData, projectData, prepareData); + + return hasNativeChanges; + } + + private emitPrepareEvent(filesChangeEventData: IFilesChangeEventData) { + if (this.isInitialPrepareReady) { + this.emit(PREPARE_READY_EVENT_NAME, filesChangeEventData); + } else { + this.persistedData.push(filesChangeEventData); + } + } +} +$injector.register("prepareController", PrepareController); diff --git a/lib/controllers/preview-app-controller.ts b/lib/controllers/preview-app-controller.ts new file mode 100644 index 0000000000..9dd961d5e1 --- /dev/null +++ b/lib/controllers/preview-app-controller.ts @@ -0,0 +1,123 @@ +import { Device, FilesPayload } from "nativescript-preview-sdk"; +import { TrackActionNames, PREPARE_READY_EVENT_NAME } from "../constants"; +import { PrepareController } from "./prepare-controller"; +import { performanceLog } from "../common/decorators"; +import { stringify } from "../common/helpers"; +import { HmrConstants } from "../common/constants"; +import { EventEmitter } from "events"; +import { PreviewAppEmitter } from "../emitters/preview-app-emitter"; +import { PrepareDataService } from "../services/prepare-data-service"; + +export class PreviewAppController extends EventEmitter { + private deviceInitializationPromise: IDictionary> = {}; + private promise = Promise.resolve(); + + constructor( + private $analyticsService: IAnalyticsService, + private $errors: IErrors, + private $hmrStatusService: IHmrStatusService, + private $logger: ILogger, + private $prepareController: PrepareController, + private $previewAppEmitter: PreviewAppEmitter, + private $previewAppFilesService: IPreviewAppFilesService, + private $previewAppLiveSyncService: IPreviewAppLiveSyncService, + private $previewAppPluginsService: IPreviewAppPluginsService, + private $previewDevicesService: IPreviewDevicesService, + private $previewSdkService: IPreviewSdkService, + private $prepareDataService: PrepareDataService + ) { super(); } + + public async preview(data: IPreviewAppLiveSyncData): Promise { + await this.$previewSdkService.initialize(data.projectDir, async (device: Device) => { + try { + if (!device) { + this.$errors.failWithoutHelp("Sending initial preview files without a specified device is not supported."); + } + + if (this.deviceInitializationPromise[device.id]) { + return this.deviceInitializationPromise[device.id]; + } + + if (device.uniqueId) { + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: TrackActionNames.PreviewAppData, + platform: device.platform, + additionalData: device.uniqueId + }); + } + + if (data.useHotModuleReload) { + this.$hmrStatusService.attachToHmrStatusEvent(); + } + + await this.$previewAppPluginsService.comparePluginsOnDevice(data, device); + + this.$prepareController.on(PREPARE_READY_EVENT_NAME, async currentPrepareData => { + await this.handlePrepareReadyEvent(data, currentPrepareData.hmrData, currentPrepareData.files, device.platform); + }); + + if (!data.env) { data.env = { }; } + data.env.externals = this.$previewAppPluginsService.getExternalPlugins(device); + + const prepareData = this.$prepareDataService.getPrepareData(data.projectDir, device.platform.toLowerCase(), { ...data, skipNativePrepare: true } ); + await this.$prepareController.prepare(prepareData); + + this.deviceInitializationPromise[device.id] = this.getInitialFilesForPlatformSafe(data, device.platform); + + try { + const payloads = await this.deviceInitializationPromise[device.id]; + return payloads; + } finally { + this.deviceInitializationPromise[device.id] = null; + } + } catch (error) { + this.$logger.trace(`Error while sending files on device ${device && device.id}. Error is`, error); + this.$previewAppEmitter.emitPreviewAppLiveSyncError(data, device.id, error, device.platform); + } + }); + return null; + } + + public async stopPreview(): Promise { + this.$previewSdkService.stop(); + this.$previewDevicesService.updateConnectedDevices([]); + } + + @performanceLog() + private async handlePrepareReadyEvent(data: IPreviewAppLiveSyncData, hmrData: IPlatformHmrData, files: string[], platform: string) { + await this.promise + .then(async () => { + const platformHmrData = _.cloneDeep(hmrData); + + this.promise = this.$previewAppLiveSyncService.syncFilesForPlatformSafe(data, { filesToSync: files }, platform); + await this.promise; + + if (data.useHotModuleReload && platformHmrData.hash) { + const devices = this.$previewDevicesService.getDevicesForPlatform(platform); + + await Promise.all(_.map(devices, async (previewDevice: Device) => { + const status = await this.$hmrStatusService.getHmrStatus(previewDevice.id, platformHmrData.hash); + if (status === HmrConstants.HMR_ERROR_STATUS) { + const originalUseHotModuleReload = data.useHotModuleReload; + data.useHotModuleReload = false; + await this.$previewAppLiveSyncService.syncFilesForPlatformSafe(data, { filesToSync: platformHmrData.fallbackFiles }, platform, previewDevice.id ); + data.useHotModuleReload = originalUseHotModuleReload; + } + })); + } + }); + } + + private async getInitialFilesForPlatformSafe(data: IPreviewAppLiveSyncData, platform: string): Promise { + this.$logger.info(`Start sending initial files for platform ${platform}.`); + + try { + const payloads = this.$previewAppFilesService.getInitialFilesPayload(data, platform); + this.$logger.info(`Successfully sent initial files for platform ${platform}.`); + return payloads; + } catch (err) { + this.$logger.warn(`Unable to apply changes for platform ${platform}. Error is: ${err}, ${stringify(err)}`); + } + } +} +$injector.register("previewAppController", PreviewAppController); diff --git a/lib/controllers/run-controller.ts b/lib/controllers/run-controller.ts new file mode 100644 index 0000000000..686183f846 --- /dev/null +++ b/lib/controllers/run-controller.ts @@ -0,0 +1,309 @@ +import { EventEmitter } from "events"; +import { LiveSyncServiceResolver } from "../resolvers/livesync-service-resolver"; +import { HmrConstants, DeviceDiscoveryEventNames } from "../common/constants"; +import { PREPARE_READY_EVENT_NAME, TrackActionNames } from "../constants"; +import { cache } from "../common/decorators"; + +export class RunController extends EventEmitter { + private processesInfo: IDictionary = {}; + + constructor( + private $analyticsService: IAnalyticsService, + private $buildDataService: IBuildDataService, + private $buildController: IBuildController, + private $deviceDebugAppService: IDeviceDebugAppService, + private $deviceInstallAppService: IDeviceInstallAppService, + private $deviceRefreshAppService: IDeviceRefreshAppService, + private $devicesService: Mobile.IDevicesService, + private $errors: IErrors, + private $hmrStatusService: IHmrStatusService, + public $hooksService: IHooksService, + private $liveSyncServiceResolver: LiveSyncServiceResolver, + private $logger: ILogger, + private $platformsDataService: IPlatformsDataService, + private $pluginsService: IPluginsService, + private $prepareController: IPrepareController, + private $prepareDataService: IPrepareDataService, + private $prepareNativePlatformService: IPrepareNativePlatformService, + private $projectDataService: IProjectDataService, + private $runEmitter: IRunEmitter + ) { super(); } + + public async run(runData: IRunData): Promise { + const { projectDir, liveSyncInfo, deviceDescriptors } = runData; + + const projectData = this.$projectDataService.getProjectData(projectDir); + await this.initializeSetup(projectData); + + const platforms = this.$devicesService.getPlatformsFromDeviceDescriptors(deviceDescriptors); + const deviceDescriptorsForInitialSync = this.getDeviceDescriptorsForInitialSync(projectDir, deviceDescriptors); + + this.persistData(projectDir, deviceDescriptors, platforms); + + const shouldStartWatcher = !liveSyncInfo.skipWatcher && !!this.processesInfo[projectDir].deviceDescriptors.length; + if (shouldStartWatcher && liveSyncInfo.useHotModuleReload) { + this.$hmrStatusService.attachToHmrStatusEvent(); + } + + this.$prepareController.on(PREPARE_READY_EVENT_NAME, async data => { + await this.syncChangedDataOnDevices(data, projectData, liveSyncInfo, deviceDescriptors); + }); + + await this.syncInitialDataOnDevices(projectData, liveSyncInfo, deviceDescriptorsForInitialSync); + + this.attachDeviceLostHandler(); + } + + public async stop(projectDir: string, deviceIdentifiers?: string[], stopOptions?: { shouldAwaitAllActions: boolean }): Promise { + const liveSyncProcessInfo = this.processesInfo[projectDir]; + if (liveSyncProcessInfo && !liveSyncProcessInfo.isStopped) { + // In case we are coming from error during livesync, the current action is the one that erred (but we are still executing it), + // so we cannot await it as this will cause infinite loop. + const shouldAwaitPendingOperation = !stopOptions || stopOptions.shouldAwaitAllActions; + + const deviceIdentifiersToRemove = deviceIdentifiers || _.map(liveSyncProcessInfo.deviceDescriptors, d => d.identifier); + + const removedDeviceIdentifiers = _.remove(liveSyncProcessInfo.deviceDescriptors, descriptor => _.includes(deviceIdentifiersToRemove, descriptor.identifier)) + .map(descriptor => descriptor.identifier); + + // Handle the case when no more devices left for any of the persisted platforms + _.each(liveSyncProcessInfo.platforms, platform => { + const devices = this.$devicesService.getDevicesForPlatform(platform); + if (!devices || !devices.length) { + this.$prepareController.stopWatchers(projectDir, platform); + } + }); + + // In case deviceIdentifiers are not passed, we should stop the whole LiveSync. + if (!deviceIdentifiers || !deviceIdentifiers.length || !liveSyncProcessInfo.deviceDescriptors || !liveSyncProcessInfo.deviceDescriptors.length) { + if (liveSyncProcessInfo.timer) { + clearTimeout(liveSyncProcessInfo.timer); + } + + _.each(liveSyncProcessInfo.platforms, platform => { + this.$prepareController.stopWatchers(projectDir, platform); + }); + + liveSyncProcessInfo.isStopped = true; + + if (liveSyncProcessInfo.actionsChain && shouldAwaitPendingOperation) { + await liveSyncProcessInfo.actionsChain; + } + + liveSyncProcessInfo.deviceDescriptors = []; + + const projectData = this.$projectDataService.getProjectData(projectDir); + await this.$hooksService.executeAfterHooks('watch', { + hookArgs: { + projectData + } + }); + } else if (liveSyncProcessInfo.currentSyncAction && shouldAwaitPendingOperation) { + await liveSyncProcessInfo.currentSyncAction; + } + + // Emit RunOnDevice stopped when we've really stopped. + _.each(removedDeviceIdentifiers, deviceIdentifier => { + this.$runEmitter.emitRunStoppedEvent(projectDir, deviceIdentifier); + }); + } + } + + public getDeviceDescriptors(projectDir: string): ILiveSyncDeviceInfo[] { + const liveSyncProcessesInfo = this.processesInfo[projectDir] || {}; + const currentDescriptors = liveSyncProcessesInfo.deviceDescriptors; + return currentDescriptors || []; + } + + private getDeviceDescriptorsForInitialSync(projectDir: string, deviceDescriptors: ILiveSyncDeviceInfo[]) { + const currentRunData = this.processesInfo[projectDir]; + const isAlreadyLiveSyncing = currentRunData && !currentRunData.isStopped; + // Prevent cases where liveSync is called consecutive times with the same device, for example [ A, B, C ] and then [ A, B, D ] - we want to execute initialSync only for D. + const deviceDescriptorsForInitialSync = isAlreadyLiveSyncing ? _.differenceBy(deviceDescriptors, currentRunData.deviceDescriptors, "identifier") : deviceDescriptors; + + return deviceDescriptorsForInitialSync; + } + + private async initializeSetup(projectData: IProjectData): Promise { + try { + await this.$pluginsService.ensureAllDependenciesAreInstalled(projectData); + } catch (err) { + this.$logger.trace(err); + this.$errors.failWithoutHelp(`Unable to install dependencies. Make sure your package.json is valid and all dependencies are correct. Error is: ${err.message}`); + } + } + + @cache() + private attachDeviceLostHandler(): void { + this.$devicesService.on(DeviceDiscoveryEventNames.DEVICE_LOST, async (device: Mobile.IDevice) => { + this.$logger.trace(`Received ${DeviceDiscoveryEventNames.DEVICE_LOST} event in LiveSync service for ${device.deviceInfo.identifier}. Will stop LiveSync operation for this device.`); + + for (const projectDir in this.processesInfo) { + try { + const deviceDescriptors = this.getDeviceDescriptors(projectDir); + if (_.find(deviceDescriptors, d => d.identifier === device.deviceInfo.identifier)) { + await this.stop(projectDir, [device.deviceInfo.identifier]); + } + } catch (err) { + this.$logger.warn(`Unable to stop LiveSync operation for ${device.deviceInfo.identifier}.`, err); + } + } + }); + } + + private async syncInitialDataOnDevices(projectData: IProjectData, liveSyncInfo: ILiveSyncInfo, deviceDescriptors: ILiveSyncDeviceInfo[]): Promise { + const deviceAction = async (device: Mobile.IDevice) => { + const deviceDescriptor = _.find(deviceDescriptors, dd => dd.identifier === device.deviceInfo.identifier); + const platformData = this.$platformsDataService.getPlatformData(device.deviceInfo.platform, projectData); + const prepareData = this.$prepareDataService.getPrepareData(liveSyncInfo.projectDir, device.deviceInfo.platform, { ...liveSyncInfo, watch: !liveSyncInfo.skipWatcher, nativePrepare: { skipNativePrepare: !!deviceDescriptor.skipNativePrepare } }); + const buildData = this.$buildDataService.getBuildData(projectData.projectDir, device.deviceInfo.platform, { ...liveSyncInfo, outputPath: deviceDescriptor.outputPath }); + const prepareResultData = await this.$prepareController.prepare(prepareData); + + try { + let packageFilePath: string = null; + const shouldBuild = prepareResultData.hasNativeChanges || await this.$buildController.shouldBuild(buildData); + if (shouldBuild) { + packageFilePath = await deviceDescriptor.buildAction(); + } else { + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: TrackActionNames.LiveSync, + device, + projectDir: projectData.projectDir + }); + } + + await this.$deviceInstallAppService.installOnDeviceIfNeeded(device, buildData, packageFilePath); + + const platformLiveSyncService = this.$liveSyncServiceResolver.resolveLiveSyncService(platformData.platformNameLowerCase); + const { force, useHotModuleReload, skipWatcher } = liveSyncInfo; + const liveSyncResultInfo = await platformLiveSyncService.fullSync({ force, useHotModuleReload, projectData, device, watch: !skipWatcher, liveSyncDeviceInfo: deviceDescriptor }); + + const refreshInfo = await this.$deviceRefreshAppService.refreshApplication(projectData, liveSyncResultInfo, deviceDescriptor); + + this.$runEmitter.emitRunExecutedEvent(projectData, device, { + syncedFiles: liveSyncResultInfo.modifiedFilesData.map(m => m.getLocalPath()), + isFullSync: liveSyncResultInfo.isFullSync + }); + + if (liveSyncResultInfo && deviceDescriptor.debuggingEnabled) { + await this.$deviceDebugAppService.enableDebugging(projectData, deviceDescriptor, refreshInfo); + } + + this.$logger.info(`Successfully synced application ${liveSyncResultInfo.deviceAppData.appIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}.`); + + this.$runEmitter.emitRunStartedEvent(projectData, device); + } catch (err) { + this.$logger.warn(`Unable to apply changes on device: ${device.deviceInfo.identifier}. Error is: ${err.message}.`); + + this.$runEmitter.emitRunErrorEvent(projectData, device, err); + } + }; + + await this.addActionToChain(projectData.projectDir, () => this.$devicesService.execute(deviceAction, (device: Mobile.IDevice) => _.some(deviceDescriptors, deviceDescriptor => deviceDescriptor.identifier === device.deviceInfo.identifier))); + } + + private async syncChangedDataOnDevices(data: IFilesChangeEventData, projectData: IProjectData, liveSyncInfo: ILiveSyncInfo, deviceDescriptors: ILiveSyncDeviceInfo[]): Promise { + const deviceAction = async (device: Mobile.IDevice) => { + const deviceDescriptor = _.find(deviceDescriptors, dd => dd.identifier === device.deviceInfo.identifier); + const platformData = this.$platformsDataService.getPlatformData(data.platform, projectData); + const prepareData = this.$prepareDataService.getPrepareData(projectData.projectDir, data.platform, { ...liveSyncInfo, watch: !liveSyncInfo.skipWatcher }); + const buildData = this.$buildDataService.getBuildData(projectData.projectDir, data.platform, { ...liveSyncInfo, outputPath: deviceDescriptor.outputPath }); + + try { + if (data.hasNativeChanges) { + await this.$prepareNativePlatformService.prepareNativePlatform(platformData, projectData, prepareData); + await this.$buildController.prepareAndBuild(buildData); + } + + const isInHMRMode = liveSyncInfo.useHotModuleReload && data.hmrData && data.hmrData.hash; + if (isInHMRMode) { + this.$hmrStatusService.watchHmrStatus(device.deviceInfo.identifier, data.hmrData.hash); + } + + const platformLiveSyncService = this.$liveSyncServiceResolver.resolveLiveSyncService(device.deviceInfo.platform); + const watchInfo = { + liveSyncDeviceInfo: deviceDescriptor, + projectData, + filesToRemove: [], + filesToSync: data.files, + isReinstalled: false, + hmrData: data.hmrData, + useHotModuleReload: liveSyncInfo.useHotModuleReload, + force: liveSyncInfo.force, + connectTimeout: 1000 + }; + let liveSyncResultInfo = await platformLiveSyncService.liveSyncWatchAction(device, watchInfo); + + await this.refreshApplication(projectData, liveSyncResultInfo, deviceDescriptor); + + if (!liveSyncResultInfo.didRecover && isInHMRMode) { + const status = await this.$hmrStatusService.getHmrStatus(device.deviceInfo.identifier, data.hmrData.hash); + if (status === HmrConstants.HMR_ERROR_STATUS) { + watchInfo.filesToSync = data.hmrData.fallbackFiles; + liveSyncResultInfo = await platformLiveSyncService.liveSyncWatchAction(device, watchInfo); + // We want to force a restart of the application. + liveSyncResultInfo.isFullSync = true; + await this.refreshApplication(projectData, liveSyncResultInfo, deviceDescriptor); + } + } + + this.$logger.info(`Successfully synced application ${liveSyncResultInfo.deviceAppData.appIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}.`); + } catch (err) { + const allErrors = (err).allErrors; + + if (allErrors && _.isArray(allErrors)) { + for (const deviceError of allErrors) { + this.$logger.warn(`Unable to apply changes for device: ${deviceError.deviceIdentifier}. Error is: ${deviceError.message}.`); + this.$runEmitter.emitRunErrorEvent(projectData, device, deviceError); + } + } + } + }; + + await this.addActionToChain(projectData.projectDir, () => this.$devicesService.execute(deviceAction, (device: Mobile.IDevice) => { + const liveSyncProcessInfo = this.processesInfo[projectData.projectDir]; + return (data.platform.toLowerCase() === device.deviceInfo.platform.toLowerCase()) && liveSyncProcessInfo && _.some(liveSyncProcessInfo.deviceDescriptors, deviceDescriptor => deviceDescriptor.identifier === device.deviceInfo.identifier); + })); + } + + private async refreshApplication(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, deviceDescriptor: ILiveSyncDeviceInfo) { + const refreshInfo = await this.$deviceRefreshAppService.refreshApplication(projectData, liveSyncResultInfo, deviceDescriptor); + + this.$runEmitter.emitRunExecutedEvent(projectData, liveSyncResultInfo.deviceAppData.device, { + syncedFiles: liveSyncResultInfo.modifiedFilesData.map(m => m.getLocalPath()), + isFullSync: liveSyncResultInfo.isFullSync + }); + + if (liveSyncResultInfo && deviceDescriptor.debuggingEnabled) { + await this.$deviceDebugAppService.enableDebugging(projectData, deviceDescriptor, refreshInfo); + } + } + + private async addActionToChain(projectDir: string, action: () => Promise): Promise { + const liveSyncInfo = this.processesInfo[projectDir]; + if (liveSyncInfo) { + liveSyncInfo.actionsChain = liveSyncInfo.actionsChain.then(async () => { + if (!liveSyncInfo.isStopped) { + liveSyncInfo.currentSyncAction = action(); + const res = await liveSyncInfo.currentSyncAction; + return res; + } + }); + + const result = await liveSyncInfo.actionsChain; + return result; + } + } + + private persistData(projectDir: string, deviceDescriptors: ILiveSyncDeviceInfo[], platforms: string[]): void { + this.processesInfo[projectDir] = this.processesInfo[projectDir] || Object.create(null); + this.processesInfo[projectDir].actionsChain = this.processesInfo[projectDir].actionsChain || Promise.resolve(); + this.processesInfo[projectDir].currentSyncAction = this.processesInfo[projectDir].actionsChain; + this.processesInfo[projectDir].isStopped = false; + this.processesInfo[projectDir].platforms = platforms; + + const currentDeviceDescriptors = this.getDeviceDescriptors(projectDir); + this.processesInfo[projectDir].deviceDescriptors = _.uniqBy(currentDeviceDescriptors.concat(deviceDescriptors), "identifier"); + } +} +$injector.register("runController", RunController); diff --git a/lib/data/build-data.ts b/lib/data/build-data.ts new file mode 100644 index 0000000000..9ab809739b --- /dev/null +++ b/lib/data/build-data.ts @@ -0,0 +1,59 @@ +import { PrepareData } from "./prepare-data"; + +export class BuildData extends PrepareData implements IBuildData { + public device?: string; + public emulator?: boolean; + public clean: boolean; + public buildForDevice?: boolean; + public buildOutputStdio?: string; + public outputPath?: string; + public copyTo?: string; + + constructor(projectDir: string, platform: string, data: any) { + super(projectDir, platform, data); + + this.device = data.device; + this.emulator = data.emulator; + this.clean = data.clean; + this.buildForDevice = data.buildForDevice; + this.buildOutputStdio = data.buildOutputStdio; + this.outputPath = data.outputPath; + this.copyTo = data.copyTo; + } +} + +export class IOSBuildData extends BuildData implements IiOSBuildData { + public teamId: string; + public provision: string; + public mobileProvisionData: any; + public buildForAppStore: boolean; + public iCloudContainerEnvironment: string; + + constructor(projectDir: string, platform: string, data: any) { + super(projectDir, platform, data); + + this.teamId = data.teamId; + this.provision = data.provision; + this.mobileProvisionData = data.mobileProvisionData; + this.buildForAppStore = data.buildForAppStore; + this.iCloudContainerEnvironment = data.iCloudContainerEnvironment; + } +} + +export class AndroidBuildData extends BuildData { + public keyStoreAlias: string; + public keyStorePath: string; + public keyStoreAliasPassword: string; + public keyStorePassword: string; + public androidBundle: boolean; + + constructor(projectDir: string, platform: string, data: any) { + super(projectDir, platform, data); + + this.keyStoreAlias = data.keyStoreAlias; + this.keyStorePath = data.keyStorePath; + this.keyStoreAliasPassword = data.keyStoreAliasPassword; + this.keyStorePassword = data.keyStorePassword; + this.androidBundle = data.androidBundle; + } +} diff --git a/lib/data/controller-data-base.ts b/lib/data/controller-data-base.ts new file mode 100644 index 0000000000..c5dfaad64f --- /dev/null +++ b/lib/data/controller-data-base.ts @@ -0,0 +1,7 @@ +export class ControllerDataBase implements IControllerDataBase { + public nativePrepare?: INativePrepare; + + constructor(public projectDir: string, public platform: string, data: any) { + this.nativePrepare = data.nativePrepare; + } +} diff --git a/lib/data/debug-data.ts b/lib/data/debug-data.ts new file mode 100644 index 0000000000..c80d2885d0 --- /dev/null +++ b/lib/data/debug-data.ts @@ -0,0 +1,3 @@ +export class DebugData { + // +} diff --git a/lib/data/platform-data.ts b/lib/data/platform-data.ts new file mode 100644 index 0000000000..89ff99a77c --- /dev/null +++ b/lib/data/platform-data.ts @@ -0,0 +1,11 @@ +import { ControllerDataBase } from "./controller-data-base"; + +export class AddPlatformData extends ControllerDataBase { + public frameworkPath?: string; + + constructor(public projectDir: string, public platform: string, data: any) { + super(projectDir, platform, data); + + this.frameworkPath = data.frameworkPath; + } +} diff --git a/lib/data/prepare-data.ts b/lib/data/prepare-data.ts new file mode 100644 index 0000000000..c24f6fcc0f --- /dev/null +++ b/lib/data/prepare-data.ts @@ -0,0 +1,36 @@ +import { ControllerDataBase } from "./controller-data-base"; + +export class PrepareData extends ControllerDataBase { + public release: boolean; + public hmr: boolean; + public env: any; + public watch?: boolean; + + constructor(public projectDir: string, public platform: string, data: any) { + super(projectDir, platform, data); + + this.release = data.release; + this.hmr = data.hmr || data.useHotModuleReload; + this.env = { + ...data.env, + hmr: data.hmr || data.useHotModuleReload + }; + this.watch = data.watch; + } +} + +export class IOSPrepareData extends PrepareData { + public teamId: string; + public provision: string; + public mobileProvisionData: any; + + constructor(projectDir: string, platform: string, data: any) { + super(projectDir, platform, data); + + this.teamId = data.teamId; + this.provision = data.provision; + this.mobileProvisionData = data.mobileProvisionData; + } +} + +export class AndroidPrepareData extends PrepareData { } diff --git a/lib/data/run-data.ts b/lib/data/run-data.ts new file mode 100644 index 0000000000..4d67d8043d --- /dev/null +++ b/lib/data/run-data.ts @@ -0,0 +1,5 @@ +export class RunData { + constructor(public projectDir: string, + public liveSyncInfo: ILiveSyncInfo, + public deviceDescriptors: ILiveSyncDeviceInfo[]) { } +} diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index 1be467010f..604cac288a 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -433,10 +433,6 @@ interface IOpener { open(target: string, appname: string): void; } -interface IBundle { - bundle: boolean; -} - interface IBundleString { bundle: string; } @@ -469,10 +465,6 @@ interface INpmInstallConfigurationOptions extends INpmInstallConfigurationOption disableNpmInstall: boolean; } -interface ICreateProjectOptions extends INpmInstallConfigurationOptionsBase { - pathToTemplate?: string; -} - interface IGenerateOptions { collection?: string; } @@ -556,7 +548,6 @@ interface IOptions extends IRelease, IDeviceIdentifier, IJustLaunch, IAvd, IAvai javascript: boolean; androidTypings: boolean; production: boolean; //npm flag - syncAllFiles: boolean; chrome: boolean; inspector: boolean; // the counterpart to --chrome background: string; @@ -578,9 +569,7 @@ interface IHasAndroidBundle { androidBundle?: boolean; } -interface IAppFilesUpdaterOptions extends IBundle, IRelease, IOptionalWatchAllFiles, IHasUseHotModuleReloadOption { } - -interface IPlatformBuildData extends IAppFilesUpdaterOptions, IBuildConfig, IEnvOptions { } +interface IPlatformBuildData extends IRelease, IHasUseHotModuleReloadOption, IBuildConfig, IEnvOptions { } interface IDeviceEmulator extends IHasEmulatorOption, IDeviceIdentifier { } @@ -912,18 +901,6 @@ interface IXcconfigService { mergeFiles(sourceFile: string, destinationFile: string): Promise; } -/** - * Describes helper used during execution of deploy commands. - */ -interface IDeployCommandHelper { - /** - * Retrieves data needed to execute deploy command. - * @param {string} platform platform to which to deploy - could be android or ios. - * @return {IDeployPlatformInfo} data needed to execute deploy command. - */ - getDeployPlatformInfo(platform: string): IDeployPlatformInfo; -} - /** * Describes helper for validating bundling. */ @@ -1026,3 +1003,39 @@ interface IRuntimeGradleVersions { interface INetworkConnectivityValidator { validate(): Promise; } + +interface IPlatformValidationService { + /** + * Ensures the passed platform is a valid one (from the supported ones) + */ + validatePlatform(platform: string, projectData: IProjectData): void; + + /** + * Gets first chance to validate the options provided as command line arguments. + * If no platform is provided or a falsy (null, undefined, "", false...) platform is provided, + * the options will be validated for all available platforms. + */ + validateOptions(provision: true | string, teamId: true | string, projectData: IProjectData, platform?: string): Promise; + + + validatePlatformInstalled(platform: string, projectData: IProjectData): void; + + /** + * Checks whether passed platform can be built on the current OS + * @param {string} platform The mobile platform. + * @param {IProjectData} projectData DTO with information about the project. + * @returns {boolean} Whether the platform is supported for current OS or not. + */ + isPlatformSupportedForOS(platform: string, projectData: IProjectData): boolean; +} + +interface IPlatformCommandHelper { + addPlatforms(platforms: string[], projectData: IProjectData, frameworkPath: string): Promise; + cleanPlatforms(platforms: string[], projectData: IProjectData, framworkPath: string): Promise; + removePlatforms(platforms: string[], projectData: IProjectData): Promise; + updatePlatforms(platforms: string[], projectData: IProjectData): Promise; + getInstalledPlatforms(projectData: IProjectData): string[]; + getAvailablePlatforms(projectData: IProjectData): string[]; + getPreparedPlatforms(projectData: IProjectData): string[]; + getCurrentPlatformVersion(platform: string, projectData: IProjectData): string; +} \ No newline at end of file diff --git a/lib/definitions/build.d.ts b/lib/definitions/build.d.ts new file mode 100644 index 0000000000..fffc6fa53e --- /dev/null +++ b/lib/definitions/build.d.ts @@ -0,0 +1,47 @@ +interface IBuildData extends IPrepareData { + device?: string; + emulator?: boolean; + clean: boolean; + buildForDevice?: boolean; + buildOutputStdio?: string; + outputPath?: string; + copyTo?: string; +} + +interface IiOSBuildData extends IBuildData { + teamId: string; + provision: string; + mobileProvisionData: any; + buildForAppStore: boolean; + iCloudContainerEnvironment: string; +} + +interface IAndroidBuildData extends IBuildData { + keyStoreAlias: string; + keyStorePath: string; + keyStoreAliasPassword: string; + keyStorePassword: string; + androidBundle: boolean; +} + +interface IBuildController { + prepareAndBuild(buildData: IBuildData): Promise; + build(buildData: IBuildData): Promise; + buildIfNeeded(buildData: IBuildData): Promise; + shouldBuild(buildData: IBuildData): Promise; +} + +interface IBuildDataService { + getBuildData(projectDir: string, platform: string, data: any): IBuildData; +} + +interface IBuildArtefactsService { + getLatestApplicationPackagePath(platformData: IPlatformData, buildOutputOptions: IBuildOutputOptions): Promise; + getAllApplicationPackages(buildOutputPath: string, validBuildOutputData: IValidBuildOutputData): IApplicationPackage[]; + copyLastOutput(targetPath: string, platformData: IPlatformData, buildOutputOptions: IBuildOutputOptions): void; +} + +interface IBuildInfoFileService { + saveBuildInfoFile(platformData: IPlatformData, buildInfoFileDirname: string): void; + getBuildInfoFromFile(platformData: IPlatformData, buildData: IBuildData): IBuildInfo; +} \ No newline at end of file diff --git a/lib/definitions/data.d.ts b/lib/definitions/data.d.ts new file mode 100644 index 0000000000..d15afc8cc2 --- /dev/null +++ b/lib/definitions/data.d.ts @@ -0,0 +1,5 @@ +interface IControllerDataBase { + projectDir: string; + platform: string; + nativePrepare?: INativePrepare; +} \ No newline at end of file diff --git a/lib/definitions/debug.d.ts b/lib/definitions/debug.d.ts index f05682ac38..469ddacd9e 100644 --- a/lib/definitions/debug.d.ts +++ b/lib/definitions/debug.d.ts @@ -13,11 +13,6 @@ interface IAppDebugData extends IProjectDir { */ applicationIdentifier: string; - /** - * Path to .app built for iOS Simulator. - */ - pathToAppPackage?: string; - /** * The name of the application, for example `MyProject`. */ diff --git a/lib/definitions/deploy.d.ts b/lib/definitions/deploy.d.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/definitions/ios.d.ts b/lib/definitions/ios.d.ts new file mode 100644 index 0000000000..9ca1fa5932 --- /dev/null +++ b/lib/definitions/ios.d.ts @@ -0,0 +1,42 @@ +import { IOSBuildData } from "../data/build-data"; + +declare global { + interface IiOSSigningService { + setupSigningForDevice(projectRoot: string, projectData: IProjectData, buildConfig: IOSBuildData): Promise; + setupSigningFromTeam(projectRoot: string, projectData: IProjectData, teamId: string): Promise; + setupSigningFromProvision(projectRoot: string, projectData: IProjectData, provision?: string, mobileProvisionData?: any): Promise; + } + + interface IXcodebuildService { + buildForSimulator(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise; + buildForDevice(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise; + buildForAppStore(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise; + } + + interface IXcodebuildArgsService { + getBuildForSimulatorArgs(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise; + getBuildForDeviceArgs(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise; + } + + interface IXcodebuildCommandService { + executeCommand(args: string[], options: IXcodebuildCommandOptions): Promise; + } + + interface IXcodebuildCommandOptions { + message?: string; + cwd: string; + stdio?: string; + spawnOptions?: any; + } + + interface IExportOptionsPlistService { + createDevelopmentExportOptionsPlist(archivePath: string, projectData: IProjectData, buildConfig: IBuildConfig): IExportOptionsPlistOutput; + createDistributionExportOptionsPlist(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig): IExportOptionsPlistOutput; + } + + interface IExportOptionsPlistOutput { + exportFileDir: string; + exportFilePath: string; + exportOptionsPlistFilePath: string; + } +} \ No newline at end of file diff --git a/lib/definitions/livesync.d.ts b/lib/definitions/livesync.d.ts index b71cb07b93..38ca7a93ba 100644 --- a/lib/definitions/livesync.d.ts +++ b/lib/definitions/livesync.d.ts @@ -1,75 +1,14 @@ import { EventEmitter } from "events"; declare global { - // This interface is a mashup of NodeJS' along with Chokidar's event watchers - interface IFSWatcher extends NodeJS.EventEmitter { - // from fs.FSWatcher - close(): void; - - /** - * events.EventEmitter - * 1. change - * 2. error - */ - addListener(event: string, listener: Function): this; - addListener(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; - addListener(event: "error", listener: (code: number, signal: string) => void): this; - - on(event: string, listener: Function): this; - on(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; - on(event: "error", listener: (code: number, signal: string) => void): this; - - once(event: string, listener: Function): this; - once(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; - once(event: "error", listener: (code: number, signal: string) => void): this; - - prependListener(event: string, listener: Function): this; - prependListener(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; - prependListener(event: "error", listener: (code: number, signal: string) => void): this; - - prependOnceListener(event: string, listener: Function): this; - prependOnceListener(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; - prependOnceListener(event: "error", listener: (code: number, signal: string) => void): this; - - // From chokidar FSWatcher - - /** - * Add files, directories, or glob patterns for tracking. Takes an array of strings or just one - * string. - */ - add(paths: string | string[]): void; - - /** - * Stop watching files, directories, or glob patterns. Takes an array of strings or just one - * string. - */ - unwatch(paths: string | string[]): void; - - /** - * Returns an object representing all the paths on the file system being watched by this - * `FSWatcher` instance. The object's keys are all the directories (using absolute paths unless - * the `cwd` option was used), and the values are arrays of the names of the items contained in - * each directory. - */ - getWatched(): IDictionary; - - /** - * Removes all listeners from watched files. - */ - close(): void; - } - - interface ILiveSyncProcessInfo { + interface IRunOnDeviceProcessInfo { timer: NodeJS.Timer; - watcherInfo: { - watcher: IFSWatcher, - patterns: string[] - }; actionsChain: Promise; isStopped: boolean; deviceDescriptors: ILiveSyncDeviceInfo[]; currentSyncAction: Promise; syncToPreviewApp: boolean; + platforms: string[]; } interface IOptionalOutputPath { @@ -128,31 +67,20 @@ declare global { /** * Whether debugging has been enabled for this device or not */ - debugggingEnabled?: boolean; - - /** - * Describes options specific for each platform, like provision for iOS, target sdk for Android, etc. - */ - platformSpecificOptions?: IPlatformOptions; - } - - interface IOptionalSkipWatcher { - /** - * Defines if the watcher should be skipped. If not passed, fs.Watcher will be started. - */ - skipWatcher?: boolean; + debuggingEnabled?: boolean; } /** * Describes a LiveSync operation. */ - interface ILiveSyncInfo extends IProjectDir, IEnvOptions, IBundle, IRelease, IOptionalSkipWatcher, IHasUseHotModuleReloadOption, IHasSyncToPreviewAppOption { + interface ILiveSyncInfo extends IProjectDir, IEnvOptions, IRelease, IHasUseHotModuleReloadOption, IHasSyncToPreviewAppOption { + emulator?: boolean; + /** - * Defines if all project files should be watched for changes. In case it is not passed, only `app` dir of the project will be watched for changes. - * In case it is set to true, the package.json of the project and node_modules directory will also be watched, so any change there will be transferred to device(s). + * Defines if the watcher should be skipped. If not passed, fs.Watcher will be started. */ - watchAllFiles?: boolean; - + skipWatcher?: boolean; + /** * Forces a build before the initial livesync. */ @@ -170,6 +98,8 @@ declare global { * If not provided, defaults to 10seconds. */ timeout?: string; + + nativePrepare?: INativePrepare; } interface IHasSyncToPreviewAppOption { @@ -196,43 +126,14 @@ declare global { isFullSync?: boolean } - interface ILatestAppPackageInstalledSettings extends IDictionary> { /* empty */ } - interface IIsEmulator { isEmulator: boolean; } - interface ILiveSyncBuildInfo extends IIsEmulator, IPlatform { - pathToBuildItem: string; - } - interface IProjectDataComposition { projectData: IProjectData; } - /** - * Desribes object that can be passed to ensureLatestAppPackageIsInstalledOnDevice method. - */ - interface IEnsureLatestAppPackageIsInstalledOnDeviceOptions extends IProjectDataComposition, IEnvOptions, IBundle, IRelease, ISkipNativeCheckOptional, IOptionalFilesToRemove, IOptionalFilesToSync { - device: Mobile.IDevice; - preparedPlatforms: string[]; - rebuiltInformation: ILiveSyncBuildInfo[]; - deviceBuildInfoDescriptor: ILiveSyncDeviceInfo; - settings: ILatestAppPackageInstalledSettings; - liveSyncData?: ILiveSyncInfo; - modifiedFiles?: string[]; - } - - /** - * Describes the action that has been executed during ensureLatestAppPackageIsInstalledOnDevice execution. - */ - interface IAppInstalledOnDeviceResult { - /** - * Defines if the app has been installed on device from the ensureLatestAppPackageIsInstalledOnDevice method. - */ - appInstalled: boolean; - } - /** * Describes LiveSync operations. */ @@ -271,50 +172,6 @@ declare global { getLiveSyncDeviceDescriptors(projectDir: string): ILiveSyncDeviceInfo[]; } - /** - * Describes LiveSync operations while debuggging. - */ - interface IDebugLiveSyncService extends ILiveSyncService { - /** - * Method used to retrieve the glob patterns which CLI will watch for file changes. Defaults to the whole app directory. - * @param {ILiveSyncInfo} liveSyncData Information needed for livesync - for example if bundle is passed or if a release build should be performed. - * @param {IProjectData} projectData Project data. - * @param {string[]} platforms Platforms to start the watcher for. - * @returns {Promise} The glob patterns. - */ - getWatcherPatterns(liveSyncData: ILiveSyncInfo, projectData: IProjectData, platforms: string[]): Promise; - - /** - * Prints debug information. - * @param {IDebugInformation} debugInformation Information to be printed. - * @returns {IDebugInformation} Full url and port where the frontend client can be connected. - */ - printDebugInformation(debugInformation: IDebugInformation): IDebugInformation; - - /** - * Enables debugging for the specified devices - * @param {IEnableDebuggingDeviceOptions[]} deviceOpts Settings used for enabling debugging for each device. - * @param {IDebuggingAdditionalOptions} enableDebuggingOptions Settings used for enabling debugging. - * @returns {Promise[]} Array of promises for each device. - */ - enableDebugging(deviceOpts: IEnableDebuggingDeviceOptions[], enableDebuggingOptions: IDebuggingAdditionalOptions): Promise[]; - - /** - * Disables debugging for the specified devices - * @param {IDisableDebuggingDeviceOptions[]} deviceOptions Settings used for disabling debugging for each device. - * @param {IDebuggingAdditionalOptions} debuggingAdditionalOptions Settings used for disabling debugging. - * @returns {Promise[]} Array of promises for each device. - */ - disableDebugging(deviceOptions: IDisableDebuggingDeviceOptions[], debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise[]; - - /** - * Attaches a debugger to the specified device. - * @param {IAttachDebuggerOptions} settings Settings used for controling the attaching process. - * @returns {Promise} Full url and port where the frontend client can be connected. - */ - attachDebugger(settings: IAttachDebuggerOptions): Promise; - } - /** * Describes additional debugging settings. */ @@ -362,7 +219,6 @@ declare global { filesToRemove: string[]; filesToSync: string[]; isReinstalled: boolean; - syncAllFiles: boolean; liveSyncDeviceInfo: ILiveSyncDeviceInfo; hmrData: IPlatformHmrData; force?: boolean; @@ -381,7 +237,6 @@ declare global { interface IFullSyncInfo extends IProjectDataComposition, IHasUseHotModuleReloadOption, IConnectTimeoutOption { device: Mobile.IDevice; watch: boolean; - syncAllFiles: boolean; liveSyncDeviceInfo: ILiveSyncDeviceInfo; force?: boolean; } @@ -404,6 +259,7 @@ declare global { shouldRestart(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise; getDeviceLiveSyncService(device: Mobile.IDevice, projectData: IProjectData): INativeScriptDeviceLiveSyncService; } + interface IRestartApplicationInfo { didRestart: boolean; } @@ -578,7 +434,6 @@ declare global { interface IDeviceProjectRootOptions { appIdentifier: string; getDirname?: boolean; - syncAllFiles?: boolean; watch?: boolean; } diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts index 650017d684..46406ade21 100644 --- a/lib/definitions/platform.d.ts +++ b/lib/definitions/platform.d.ts @@ -14,233 +14,14 @@ interface IBuildPlatformAction { buildPlatform(platform: string, buildConfig: IBuildConfig, projectData: IProjectData): Promise; } -interface IPlatformService extends IBuildPlatformAction, NodeJS.EventEmitter { - cleanPlatforms(platforms: string[], projectData: IProjectData, config: IPlatformOptions, framework?: string): Promise; - - addPlatforms(platforms: string[], projectData: IProjectData, config: IPlatformOptions, frameworkPath?: string): Promise; - - /** - * Gets list of all installed platforms (the ones for which /platforms/ exists). - * @param {IProjectData} projectData DTO with information about the project. - * @returns {string[]} List of currently installed platforms. - */ - getInstalledPlatforms(projectData: IProjectData): string[]; - - /** - * Gets a list of all platforms that can be used on current OS, but are not installed at the moment. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {string[]} List of all available platforms. - */ - getAvailablePlatforms(projectData: IProjectData): string[]; - - /** - * Returns a list of all currently prepared platforms. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {string[]} List of all prepared platforms. - */ - getPreparedPlatforms(projectData: IProjectData): string[]; - - /** - * Remove platforms from specified project (`/platforms/` dir). - * @param {string[]} platforms Platforms to be removed. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {Promise} - */ - removePlatforms(platforms: string[], projectData: IProjectData): Promise; - - updatePlatforms(platforms: string[], projectData: IProjectData, config: IPlatformOptions): Promise; - - /** - * Ensures that the specified platform and its dependencies are installed. - * When there are changes to be prepared, it prepares the native project for the specified platform. - * When finishes, prepare saves the .nsprepareinfo file in platform folder. - * This file contains information about current project configuration and allows skipping unnecessary build, deploy and livesync steps. - * @param {IPreparePlatformInfo} platformInfo Options to control the preparation. - * @returns {boolean} true indicates that the platform was prepared. - */ - preparePlatform(platformInfo: IPreparePlatformInfo): Promise; - - /** - * Determines whether a build is necessary. A build is necessary when one of the following is true: - * - there is no previous build. - * - the .nsbuildinfo file in product folder points to an old prepare. - * @param {string} platform The platform to build. - * @param {IProjectData} projectData DTO with information about the project. - * @param {IBuildConfig} @optional buildConfig Indicates whether the build is for device or emulator. - * @param {string} @optional outputPath Directory containing build information and artifacts. - * @returns {boolean} true indicates that the platform should be build. - */ - shouldBuild(platform: string, projectData: IProjectData, buildConfig?: IBuildConfig, outputPath?: string): Promise; - - /** - * Determines whether installation is necessary. It is necessary when one of the following is true: - * - the application is not installed. - * - the .nsbuildinfo file located in application root folder is different than the local .nsbuildinfo file - * @param {Mobile.IDevice} device The device where the application should be installed. - * @param {IProjectData} projectData DTO with information about the project. - * @param {string} @optional outputPath Directory containing build information and artifacts. - * @returns {Promise} true indicates that the application should be installed. - */ - shouldInstall(device: Mobile.IDevice, projectData: IProjectData, release: IRelease, outputPath?: string): Promise; - - /** - * - * @param {Mobile.IDevice} device The device where the application should be installed. - * @param {IProjectData} projectData DTO with information about the project. - * @param {string} @optional outputPath Directory containing build information and artifacts. - */ - validateInstall(device: Mobile.IDevice, projectData: IProjectData, release: IRelease, outputPath?: string): Promise; - - /** - * Determines whether the project should undergo the prepare process. - * @param {IShouldPrepareInfo} shouldPrepareInfo Options needed to decide whether to prepare. - * @returns {Promise} true indicates that the project should be prepared. - */ - shouldPrepare(shouldPrepareInfo: IShouldPrepareInfo): Promise; - - /** - * Installs the application on specified device. - * When finishes, saves .nsbuildinfo in application root folder to indicate the prepare that was used to build the app. - * * .nsbuildinfo is not persisted when building for release. - * @param {Mobile.IDevice} device The device where the application should be installed. - * @param {IBuildConfig} options The build configuration. - * @param {string} @optional pathToBuiltApp Path to build artifact. - * @param {string} @optional outputPath Directory containing build information and artifacts. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {void} - */ - installApplication(device: Mobile.IDevice, options: IBuildConfig, projectData: IProjectData, pathToBuiltApp?: string, outputPath?: string): Promise; - - /** - * Gets first chance to validate the options provided as command line arguments. - * If no platform is provided or a falsy (null, undefined, "", false...) platform is provided, - * the options will be validated for all available platforms. - */ - validateOptions(provision: true | string, teamId: true | string, projectData: IProjectData, platform?: string): Promise; - - /** - * Executes prepare, build and installOnPlatform when necessary to ensure that the latest version of the app is installed on specified platform. - * - When --clean option is specified it builds the app on every change. If not, build is executed only when there are native changes. - * @param {IDeployPlatformInfo} deployInfo Options required for project preparation and deployment. - * @returns {void} - */ - deployPlatform(deployInfo: IDeployPlatformInfo): Promise; - - /** - * Runs the application on specified platform. Assumes that the application is already build and installed. Fails if this is not true. - * @param {string} platform The platform where to start the application. - * @param {IRunPlatformOptions} runOptions Various options that help manage the run operation. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {void} - */ - startApplication(platform: string, runOptions: IRunPlatformOptions, appData: Mobile.IStartApplicationData): Promise; - - cleanDestinationApp(platformInfo: IPreparePlatformInfo): Promise; - validatePlatformInstalled(platform: string, projectData: IProjectData): void; - - /** - * Ensures the passed platform is a valid one (from the supported ones) - */ - validatePlatform(platform: string, projectData: IProjectData): void; - - /** - * Checks whether passed platform can be built on the current OS - * @param {string} platform The mobile platform. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {boolean} Whether the platform is supported for current OS or not. - */ - isPlatformSupportedForOS(platform: string, projectData: IProjectData): boolean; - - /** - * Returns information about the latest built application for device in the current project. - * @param {IPlatformData} platformData Data describing the current platform. - * @param {IBuildConfig} buildConfig Defines if the build is for release configuration. - * @param {string} @optional outputPath Directory that should contain the build artifact. - * @returns {IApplicationPackage} Information about latest built application. - */ - getLatestApplicationPackageForDevice(platformData: IPlatformData, buildConfig: IBuildConfig, outputPath?: string): IApplicationPackage; - - /** - * Returns information about the latest built application for simulator in the current project. - * @param {IPlatformData} platformData Data describing the current platform. - * @param {IBuildConfig} buildConfig Defines if the build is for release configuration. - * @param {string} @optional outputPath Directory that should contain the build artifact. - * @returns {IApplicationPackage} Information about latest built application. - */ - getLatestApplicationPackageForEmulator(platformData: IPlatformData, buildConfig: IBuildConfig, outputPath?: string): IApplicationPackage; - - /** - * Copies latest build output to a specified location. - * @param {string} platform Mobile platform - Android, iOS. - * @param {string} targetPath Destination where the build artifact should be copied. - * @param {IBuildConfig} buildConfig Defines if the searched artifact should be for simulator and is it built for release. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {void} - */ - copyLastOutput(platform: string, targetPath: string, buildConfig: IBuildConfig, projectData: IProjectData): void; - - /** - * Gets the latest build output. - * @param {string} platform Mobile platform - Android, iOS. - * @param {IBuildConfig} buildConfig Defines if the searched artifact should be for simulator and is it built for release. - * @param {IProjectData} projectData DTO with information about the project. - * @param {string} @optional outputPath Directory that should contain the build artifact. - * @returns {string} The path to latest built artifact. - */ - lastOutputPath(platform: string, buildConfig: IBuildConfig, projectData: IProjectData, outputPath?: string): string; - - /** - * Reads contents of a file on device. - * @param {Mobile.IDevice} device The device to read from. - * @param {string} deviceFilePath The file path. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {string} The contents of the file or null when there is no such file. - */ - readFile(device: Mobile.IDevice, deviceFilePath: string, projectData: IProjectData): Promise; - - /** - * Saves build information in a proprietary file. - * @param {string} platform The build platform. - * @param {string} projectDir The project's directory. - * @param {string} buildInfoFileDirname The directory where the build file should be written to. - * @returns {void} - */ - saveBuildInfoFile(platform: string, projectDir: string, buildInfoFileDirname: string): void; - - /** - * Gives information for the current version of the runtime. - * @param {string} platform The platform to be checked. - * @param {IProjectData} projectData The data describing the project - * @returns {string} Runtime version - */ - getCurrentPlatformVersion(platform: string, projectData: IProjectData): string; -} - -interface IPlatformOptions extends IPlatformSpecificData, ICreateProjectOptions { } - -/** - * Platform specific data required for project preparation. - */ -interface IPlatformSpecificData extends IProvision, ITeamIdentifier { - /** - * Target SDK for Android. - */ - sdk: string; - - /** - * Data from mobileProvision. - */ - mobileProvisionData?: any; -} - interface IPlatformData { frameworkPackageName: string; platformProjectService: IPlatformProjectService; projectRoot: string; normalizedPlatformName: string; + platformNameLowerCase: string; appDestinationDirectoryPath: string; getBuildOutputPath(options: IBuildOutputOptions): string; - bundleBuildOutputPath?: string; getValidBuildOutputData(buildOptions: IBuildOutputOptions): IValidBuildOutputData; frameworkFilesExtensions: string[]; frameworkDirectoriesExtensions?: string[]; @@ -257,32 +38,16 @@ interface IValidBuildOutputData { regexes?: RegExp[]; } -interface IBuildOutputOptions extends Partial, IRelease, IHasAndroidBundle { } - -interface IPlatformsData { - availablePlatforms: any; - platformsNames: string[]; - getPlatformData(platform: string, projectData: IProjectData): IPlatformData; +interface IBuildOutputOptions extends Partial, IRelease, IHasAndroidBundle { + outputPath?: string; } -interface IAppFilesUpdaterOptionsComposition { - appFilesUpdaterOptions: IAppFilesUpdaterOptions; -} - -interface INodeModulesData extends IPlatform, IProjectDataComposition, IAppFilesUpdaterOptionsComposition { - absoluteOutputPath: string; - lastModifiedTime: Date; - projectFilesConfig: IProjectFilesConfig; -} - -interface INodeModulesBuilderData { - nodeModulesData: INodeModulesData; - release: boolean; - copyNodeModules?: boolean; +interface IPlatformsDataService { + getPlatformData(platform: string, projectData: IProjectData): IPlatformData; } interface INodeModulesBuilder { - prepareNodeModules(opts: INodeModulesBuilderData): Promise; + prepareNodeModules(platformData: IPlatformData, projectData: IProjectData): Promise; } interface INodeModulesDependenciesBuilder { @@ -301,71 +66,6 @@ interface IBuildInfo { deploymentTarget?: string; } -interface IPlatformDataComposition { - platformData: IPlatformData; -} - -interface ICopyAppFilesData extends IProjectDataComposition, IAppFilesUpdaterOptionsComposition, IPlatformDataComposition, IOptionalFilesToSync, IOptionalFilesToRemove { } - -interface IPreparePlatformService { - addPlatform(info: IAddPlatformInfo): Promise; - preparePlatform(config: IPreparePlatformJSInfo): Promise; -} - -interface IAddPlatformInfo extends IProjectDataComposition, IPlatformDataComposition { - frameworkDir: string; - installedVersion: string; - config: IPlatformOptions; -} - -interface IPreparePlatformJSInfo extends IPreparePlatformCoreInfo, ICopyAppFilesData { - projectFilesConfig?: IProjectFilesConfig; -} - -interface IShouldPrepareInfo extends IOptionalProjectChangesInfoComposition { - platformInfo: IPreparePlatformInfo; -} - -interface IOptionalProjectChangesInfoComposition { - changesInfo?: IProjectChangesInfo; -} - -interface IPreparePlatformCoreInfo extends IPreparePlatformInfoBase, IOptionalProjectChangesInfoComposition { - platformSpecificData: IPlatformSpecificData; -} - -interface IPreparePlatformInfo extends IPreparePlatformInfoBase, IPlatformConfig, ISkipNativeCheckOptional { } - -interface IPlatformConfig { - config: IPlatformOptions; -} - -interface IOptionalFilesToSync { - filesToSync?: string[]; -} - -interface IOptionalFilesToRemove { - filesToRemove?: string[]; -} - -interface IPreparePlatformInfoBase extends IPlatform, IAppFilesUpdaterOptionsComposition, IProjectDataComposition, IEnvOptions, IOptionalFilesToSync, IOptionalFilesToRemove, IOptionalNativePrepareComposition { } - -interface IOptionalNativePrepareComposition { - nativePrepare?: INativePrepare; -} - -interface IOptionalWatchAllFiles { - watchAllFiles?: boolean; -} - -interface IDeployPlatformInfo extends IPlatform, IAppFilesUpdaterOptionsComposition, IProjectDataComposition, IPlatformConfig, IEnvOptions, IOptionalNativePrepareComposition, IOptionalOutputPath, IBuildPlatformAction { - deployOptions: IDeployPlatformOptions -} - -interface IUpdateAppOptions extends IOptionalFilesToSync, IOptionalFilesToRemove { - beforeCopyAction: (sourceFiles: string[]) => void; -} - interface IPlatformEnvironmentRequirements { checkEnvironmentRequirements(input: ICheckEnvironmentRequirementsInput): Promise; } @@ -382,3 +82,16 @@ interface ICheckEnvironmentRequirementsOutput { canExecute: boolean; selectedOption: string; } + +interface IAddPlatformData extends IControllerDataBase { + frameworkPath?: string; +} + +interface IPlatformController { + addPlatform(addPlatformData: IAddPlatformData): Promise; + addPlatformIfNeeded(addPlatformData: IAddPlatformData): Promise; +} + +interface IAddPlatformService { + addPlatformSafe(projectData: IProjectData, platformData: IPlatformData, packageToInstall: string, nativePrepare: INativePrepare): Promise; +} diff --git a/lib/definitions/plugins.d.ts b/lib/definitions/plugins.d.ts index 481f56057b..dd59b45d32 100644 --- a/lib/definitions/plugins.d.ts +++ b/lib/definitions/plugins.d.ts @@ -26,7 +26,7 @@ interface IBasePluginData { } interface IPluginData extends INodeModuleData { - platformsData: IPluginPlatformsData; + platformsDataService: IPluginPlatformsData; /* Gets all plugin variables from plugin */ pluginVariables: IDictionary; pluginPlatformsFolderPath(platform: string): string; diff --git a/lib/definitions/prepare.d.ts b/lib/definitions/prepare.d.ts new file mode 100644 index 0000000000..15c6c981b9 --- /dev/null +++ b/lib/definitions/prepare.d.ts @@ -0,0 +1,37 @@ +import { EventEmitter } from "events"; + +declare global { + + interface IPrepareData extends IControllerDataBase { + release: boolean; + hmr: boolean; + env: any; + watch?: boolean; + } + + interface IiOSPrepareData extends IPrepareData { + teamId: string; + provision: string; + mobileProvisionData: any; + } + + interface IAndroidPrepareData extends IPrepareData { } + + interface IPrepareDataService { + getPrepareData(projectDir: string, platform: string, data: any): IPrepareData; + } + + interface IPrepareController extends EventEmitter { + prepare(prepareData: IPrepareData): Promise; + stopWatchers(projectDir: string, platform: string): void; + } + + interface IPrepareResultData { + platform: string; + hasNativeChanges: boolean; + } + + interface IPrepareNativePlatformService { + prepareNativePlatform(platformData: IPlatformData, projectData: IProjectData, prepareData: IPrepareData): Promise; + } +} \ No newline at end of file diff --git a/lib/definitions/preview-app-livesync.d.ts b/lib/definitions/preview-app-livesync.d.ts index 123ae2e721..1fb10a0972 100644 --- a/lib/definitions/preview-app-livesync.d.ts +++ b/lib/definitions/preview-app-livesync.d.ts @@ -3,9 +3,8 @@ import { EventEmitter } from "events"; declare global { interface IPreviewAppLiveSyncService extends EventEmitter { - initialize(data: IPreviewAppLiveSyncData): void; syncFiles(data: IPreviewAppLiveSyncData, filesToSync: string[], filesToRemove: string[]): Promise; - stopLiveSync(): Promise; + syncFilesForPlatformSafe(data: IPreviewAppLiveSyncData, filesData: IPreviewAppFilesData, platform: string, deviceId?: string): Promise; } interface IPreviewAppFilesService { @@ -18,7 +17,7 @@ declare global { filesToRemove?: string[]; } - interface IPreviewAppLiveSyncData extends IProjectDir, IHasUseHotModuleReloadOption, IBundle, IEnvOptions { } + interface IPreviewAppLiveSyncData extends IProjectDir, IHasUseHotModuleReloadOption, IEnvOptions { } interface IPreviewSdkService extends EventEmitter { getQrCodeUrl(options: IGetQrCodeUrlOptions): string; diff --git a/lib/definitions/project-changes.d.ts b/lib/definitions/project-changes.d.ts index 7da52ca180..8841e98b41 100644 --- a/lib/definitions/project-changes.d.ts +++ b/lib/definitions/project-changes.d.ts @@ -13,13 +13,11 @@ interface IPrepareInfo extends IAddedNativePlatform, IAppFilesHashes { } interface IProjectChangesInfo extends IAddedNativePlatform { - appFilesChanged: boolean; appResourcesChanged: boolean; modulesChanged: boolean; configChanged: boolean; packageChanged: boolean; nativeChanged: boolean; - bundleChanged: boolean; signingChanged: boolean; readonly hasChanges: boolean; @@ -27,33 +25,6 @@ interface IProjectChangesInfo extends IAddedNativePlatform { readonly changesRequirePrepare: boolean; } -/** - * Describes interface for controlling checking node_modules for native changes. - */ -interface ISkipNativeCheckOptional { - /** - * Designates node_modules should not be checked for native changes. - */ - skipModulesNativeCheck?: boolean; -} - -interface IProjectChangesOptions extends IAppFilesUpdaterOptions, IProvision, ITeamIdentifier, ISkipNativeCheckOptional { - nativePlatformStatus?: "1" | "2" | "3"; -} - -interface ICheckForChangesOptions extends IPlatform, IProjectDataComposition { - projectChangesOptions: IProjectChangesOptions; -} - -interface IProjectChangesService { - checkForChanges(checkForChangesOpts: ICheckForChangesOptions): Promise; - getPrepareInfo(platform: string, projectData: IProjectData): IPrepareInfo; - savePrepareInfo(platform: string, projectData: IProjectData): void; - getPrepareInfoFilePath(platform: string, projectData: IProjectData): string; - setNativePlatformStatus(platform: string, projectData: IProjectData, nativePlatformStatus: IAddedNativePlatform): void; - currentChanges: IProjectChangesInfo; -} - /** * NativePlatformStatus.requiresPlatformAdd | NativePlatformStatus.requiresPrepare | NativePlatformStatus.alreadyPrepared */ diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index f68e51b970..e19e5fb7d8 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -353,128 +353,12 @@ interface ILocalBuildService { interface ICleanNativeAppData extends IProjectDir, IPlatform { } -interface IPlatformProjectService extends NodeJS.EventEmitter, IPlatformProjectServiceBase { - getPlatformData(projectData: IProjectData): IPlatformData; - validate(projectData: IProjectData, options: IOptions, notConfiguredEnvOptions?: INotConfiguredEnvOptions): Promise; - createProject(frameworkDir: string, frameworkVersion: string, projectData: IProjectData, config: ICreateProjectOptions): Promise; - interpolateData(projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise; - interpolateConfigurationFile(projectData: IProjectData, platformSpecificData: IPlatformSpecificData): void; - - /** - * Executes additional actions after native project is created. - * @param {string} projectRoot Path to the real NativeScript project. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {void} - */ - afterCreateProject(projectRoot: string, projectData: IProjectData): void; - - /** - * Gets first chance to validate the options provided as command line arguments. - * @param {string} projectId Project identifier - for example org.nativescript.test. - * @param {any} provision UUID of the provisioning profile used in iOS option validation. - * @returns {void} - */ - validateOptions(projectId?: string, provision?: true | string, teamId?: true | string): Promise; - - buildProject(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig): Promise; - - /** - * Prepares images in Native project (for iOS). - * @param {IProjectData} projectData DTO with information about the project. - * @param {any} platformSpecificData Platform specific data required for project preparation. - * @returns {void} - */ - prepareProject(projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise; - - /** - * Prepares App_Resources in the native project by clearing data from other platform and applying platform specific rules. - * @param {string} appResourcesDirectoryPath The place in the native project where the App_Resources are copied first. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {void} - */ - prepareAppResources(appResourcesDirectoryPath: string, projectData: IProjectData): void; - - /** - * Defines if current platform is prepared (i.e. if /platforms/ dir exists). - * @param {string} projectRoot The project directory (path where root's package.json is located). - * @param {IProjectData} projectData DTO with information about the project. - * @returns {boolean} True in case platform is prepare (i.e. if /platforms/ dir exists), false otherwise. - */ - isPlatformPrepared(projectRoot: string, projectData: IProjectData): boolean; - - /** - * Checks if current platform can be updated to a newer versions. - * @param {string} newInstalledModuleDir Path to the native project. - * @param {IProjectData} projectData DTO with information about the project. - * @return {boolean} True if platform can be updated. false otherwise. - */ - canUpdatePlatform(newInstalledModuleDir: string, projectData: IProjectData): boolean; - - preparePluginNativeCode(pluginData: IPluginData, options?: any): Promise; - - /** - * Removes native code of a plugin (CocoaPods, jars, libs, src). - * @param {IPluginData} Plugins data describing the plugin which should be cleaned. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {void} - */ - removePluginNativeCode(pluginData: IPluginData, projectData: IProjectData): Promise; - - beforePrepareAllPlugins(projectData: IProjectData, dependencies?: IDependencyData[]): Promise; - - handleNativeDependenciesChange(projectData: IProjectData, opts: IRelease): Promise; - - /** - * Gets the path wheren App_Resources should be copied. - * @returns {string} Path to native project, where App_Resources should be copied. - */ - getAppResourcesDestinationDirectoryPath(projectData: IProjectData): string; - - cleanDeviceTempFolder(deviceIdentifier: string, projectData: IProjectData): Promise; - processConfigurationFilesFromAppResources(projectData: IProjectData, opts: { release: boolean }): Promise; - - /** - * Ensures there is configuration file (AndroidManifest.xml, Info.plist) in app/App_Resources. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {void} - */ - ensureConfigurationFileInAppResources(projectData: IProjectData): void; - - /** - * Stops all running processes that might hold a lock on the filesystem. - * Android: Gradle daemon processes are terminated. - * @param {IPlatformData} platformData The data for the specified platform. - * @returns {void} - */ - stopServices?(projectRoot: string): Promise; - - /** - * Removes build artifacts specific to the platform - * @param {string} projectRoot The root directory of the native project. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {void} - */ - cleanProject?(projectRoot: string, projectData: IProjectData): Promise - - /** - * Check the current state of the project, and validate against the options. - * If there are parts in the project that are inconsistent with the desired options, marks them in the changeset flags. - */ - checkForChanges(changeset: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): Promise; - - /** - * Get the deployment target's version - * Currently implemented only for iOS -> returns the value of IPHONEOS_DEPLOYMENT_TARGET property from xcconfig file - */ - getDeploymentTarget?(projectData: IProjectData): any; -} - interface IValidatePlatformOutput { checkEnvironmentRequirementsOutput: ICheckEnvironmentRequirementsOutput; } interface ITestExecutionService { - startKarmaServer(platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise; + startKarmaServer(platform: string, liveSyncInfo: ILiveSyncInfo, deviceDescriptors: ILiveSyncDeviceInfo[]): Promise; canStartKarmaServer(projectData: IProjectData): Promise; } diff --git a/lib/definitions/run.d.ts b/lib/definitions/run.d.ts new file mode 100644 index 0000000000..5ef09b0ab1 --- /dev/null +++ b/lib/definitions/run.d.ts @@ -0,0 +1,37 @@ +interface IRunData { + projectDir: string; + liveSyncInfo: ILiveSyncInfo; + deviceDescriptors: ILiveSyncDeviceInfo[]; +} + +interface IRunController { + +} + +interface IRunEmitter { + emitRunStartedEvent(projectData: IProjectData, device: Mobile.IDevice): void; + emitRunNotificationEvent(projectData: IProjectData, device: Mobile.IDevice, notification: string): void; + emitRunErrorEvent(projectData: IProjectData, device: Mobile.IDevice, error: Error): void; + emitRunExecutedEvent(projectData: IProjectData, device: Mobile.IDevice, options: { syncedFiles: string[], isFullSync: boolean }): void; + emitRunStoppedEvent(projectDir: string, deviceIdentifier: string): void; + emitDebuggerAttachedEvent(debugInformation: IDebugInformation): void; + emitDebuggerDetachedEvent(device: Mobile.IDevice): void; + emitUserInteractionNeededEvent(projectData: IProjectData, device: Mobile.IDevice, deviceDescriptor: ILiveSyncDeviceInfo): void; +} + +interface IDeviceInstallAppService { + installOnDevice(device: Mobile.IDevice, buildData: IBuildData, packageFile?: string): Promise; + installOnDeviceIfNeeded(device: Mobile.IDevice, buildData: IBuildData, packageFile?: string): Promise; + getDeviceBuildInfoFilePath(device: Mobile.IDevice, projectData: IProjectData): Promise; + shouldInstall(device: Mobile.IDevice, buildData: IBuildData): Promise; +} + +interface IDeviceRefreshAppService { + refreshApplication(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, deviceDescriptor: ILiveSyncDeviceInfo, settings?: IRefreshApplicationSettings): Promise; +} + +interface IDeviceDebugAppService { + enableDebugging(projectData: IProjectData, deviceDescriptor: ILiveSyncDeviceInfo, refreshInfo: IRestartApplicationInfo): Promise; + attachDebugger(settings: IAttachDebuggerOptions): Promise; + printDebugInformation(debugInformation: IDebugInformation, fireDebuggerAttachedEvent: boolean): IDebugInformation; +} \ No newline at end of file diff --git a/lib/definitions/xcode.d.ts b/lib/definitions/xcode.d.ts index d1d524d45e..7bcf86ca6e 100644 --- a/lib/definitions/xcode.d.ts +++ b/lib/definitions/xcode.d.ts @@ -58,43 +58,4 @@ declare module "nativescript-dev-xcode" { uuid: string; pbxGroup: Object; } -} - -interface IiOSSigningService { - setupSigningForDevice(projectRoot: string, projectData: IProjectData, buildConfig: IiOSBuildConfig): Promise; - setupSigningFromTeam(projectRoot: string, projectData: IProjectData, teamId: string): Promise; - setupSigningFromProvision(projectRoot: string, projectData: IProjectData, provision?: string, mobileProvisionData?: any): Promise; -} - -interface IXcodebuildService { - buildForSimulator(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise; - buildForDevice(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise; - buildForAppStore(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise; -} - -interface IXcodebuildArgsService { - getBuildForSimulatorArgs(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise; - getBuildForDeviceArgs(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise; -} - -interface IXcodebuildCommandService { - executeCommand(args: string[], options: IXcodebuildCommandOptions): Promise; -} - -interface IXcodebuildCommandOptions { - message?: string; - cwd: string; - stdio?: string; - spawnOptions?: any; -} - -interface IExportOptionsPlistService { - createDevelopmentExportOptionsPlist(archivePath: string, projectData: IProjectData, buildConfig: IBuildConfig): IExportOptionsPlistOutput; - createDistributionExportOptionsPlist(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig): IExportOptionsPlistOutput; -} - -interface IExportOptionsPlistOutput { - exportFileDir: string; - exportFilePath: string; - exportOptionsPlistFilePath: string; } \ No newline at end of file diff --git a/lib/device-path-provider.ts b/lib/device-path-provider.ts index bdd9c24721..b80ec0fe90 100644 --- a/lib/device-path-provider.ts +++ b/lib/device-path-provider.ts @@ -25,7 +25,7 @@ export class DevicePathProvider implements IDevicePathProvider { projectRoot = `${LiveSyncPaths.ANDROID_TMP_DIR_NAME}/${options.appIdentifier}`; if (!options.getDirname) { const hashService = (device).fileSystem.getDeviceHashService(options.appIdentifier); - const hashFile = options.syncAllFiles ? null : await hashService.doesShasumFileExistsOnDevice(); + const hashFile = await hashService.doesShasumFileExistsOnDevice(); const syncFolderName = options.watch || hashFile ? LiveSyncPaths.SYNC_DIR_NAME : LiveSyncPaths.FULLSYNC_DIR_NAME; projectRoot = path.join(projectRoot, syncFolderName); } diff --git a/lib/emitters/preview-app-emitter.ts b/lib/emitters/preview-app-emitter.ts new file mode 100644 index 0000000000..99923fbef2 --- /dev/null +++ b/lib/emitters/preview-app-emitter.ts @@ -0,0 +1,14 @@ +import { EventEmitter } from "events"; +import { PreviewAppLiveSyncEvents } from "../services/livesync/playground/preview-app-constants"; + +export class PreviewAppEmitter extends EventEmitter { + public emitPreviewAppLiveSyncError(data: IPreviewAppLiveSyncData, deviceId: string, error: Error, platform?: string) { + this.emit(PreviewAppLiveSyncEvents.PREVIEW_APP_LIVE_SYNC_ERROR, { + error, + data, + platform, + deviceId + }); + } +} +$injector.register("previewAppEmitter", PreviewAppEmitter); diff --git a/lib/emitters/run-emitter.ts b/lib/emitters/run-emitter.ts new file mode 100644 index 0000000000..947222292a --- /dev/null +++ b/lib/emitters/run-emitter.ts @@ -0,0 +1,79 @@ +import { EventEmitter } from "events"; +import { RunOnDeviceEvents, DEBUGGER_DETACHED_EVENT_NAME, USER_INTERACTION_NEEDED_EVENT_NAME, DEBUGGER_ATTACHED_EVENT_NAME } from "../constants"; + +export class RunEmitter extends EventEmitter implements IRunEmitter { + constructor( + private $logger: ILogger + ) { super(); } + + public emitRunStartedEvent(projectData: IProjectData, device: Mobile.IDevice): void { + this.emitCore(RunOnDeviceEvents.runOnDeviceStarted, { + projectDir: projectData.projectDir, + deviceIdentifier: device.deviceInfo.identifier, + applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()] + }); + } + + public emitRunNotificationEvent(projectData: IProjectData, device: Mobile.IDevice, notification: string): void { + this.emitCore(RunOnDeviceEvents.runOnDeviceNotification, { + projectDir: projectData.projectDir, + deviceIdentifier: device.deviceInfo.identifier, + applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()], + notification + }); + } + + public emitRunErrorEvent(projectData: IProjectData, device: Mobile.IDevice, error: Error): void { + this.emitCore(RunOnDeviceEvents.runOnDeviceError, { + projectDir: projectData.projectDir, + deviceIdentifier: device.deviceInfo.identifier, + applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()], + error, + }); + } + + public emitRunExecutedEvent(projectData: IProjectData, device: Mobile.IDevice, options: { syncedFiles: string[], isFullSync: boolean }): void { + this.emitCore(RunOnDeviceEvents.runOnDeviceExecuted, { + projectDir: projectData.projectDir, + deviceIdentifier: device.deviceInfo.identifier, + applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()], + syncedFiles: options.syncedFiles, + isFullSync: options.isFullSync + }); + } + + public emitRunStoppedEvent(projectDir: string, deviceIdentifier: string): void { + this.emitCore(RunOnDeviceEvents.runOnDeviceStopped, { + projectDir, + deviceIdentifier + }); + } + + public emitDebuggerAttachedEvent(debugInformation: IDebugInformation): void { + this.emit(DEBUGGER_ATTACHED_EVENT_NAME, debugInformation); + } + + public emitDebuggerDetachedEvent(device: Mobile.IDevice): void { + const deviceIdentifier = device.deviceInfo.identifier; + this.emit(DEBUGGER_DETACHED_EVENT_NAME, { deviceIdentifier }); + } + + public emitUserInteractionNeededEvent(projectData: IProjectData, device: Mobile.IDevice, deviceDescriptor: ILiveSyncDeviceInfo): void { + const deviceIdentifier = device.deviceInfo.identifier; + const attachDebuggerOptions: IAttachDebuggerOptions = { + platform: device.deviceInfo.platform, + isEmulator: device.isEmulator, + projectDir: projectData.projectDir, + deviceIdentifier, + debugOptions: deviceDescriptor.debugOptions, + outputPath: deviceDescriptor.outputPath + }; + this.emit(USER_INTERACTION_NEEDED_EVENT_NAME, attachDebuggerOptions); + } + + private emitCore(event: string, data: ILiveSyncEventData): void { + this.$logger.trace(`Will emit event ${event} with data`, data); + this.emit(event, data); + } +} +$injector.register("runEmitter", RunEmitter); diff --git a/lib/helpers/bundle-validator-helper.ts b/lib/helpers/bundle-validator-helper.ts index 4eedac652b..241ac84598 100644 --- a/lib/helpers/bundle-validator-helper.ts +++ b/lib/helpers/bundle-validator-helper.ts @@ -15,19 +15,17 @@ export class BundleValidatorHelper extends VersionValidatorHelper implements IBu } public validate(minSupportedVersion?: string): void { - if (this.$options.bundle) { - const bundlePluginName = this.bundlersMap[this.$options.bundle]; - const bundlerVersionInDependencies = this.$projectData.dependencies && this.$projectData.dependencies[bundlePluginName]; - const bundlerVersionInDevDependencies = this.$projectData.devDependencies && this.$projectData.devDependencies[bundlePluginName]; - if (!bundlePluginName || (!bundlerVersionInDependencies && !bundlerVersionInDevDependencies)) { - this.$errors.failWithoutHelp(BundleValidatorMessages.MissingBundlePlugin); - } + const bundlePluginName = this.bundlersMap["webpack"]; + const bundlerVersionInDependencies = this.$projectData.dependencies && this.$projectData.dependencies[bundlePluginName]; + const bundlerVersionInDevDependencies = this.$projectData.devDependencies && this.$projectData.devDependencies[bundlePluginName]; + if (!bundlePluginName || (!bundlerVersionInDependencies && !bundlerVersionInDevDependencies)) { + this.$errors.failWithoutHelp(BundleValidatorMessages.MissingBundlePlugin); + } - const currentVersion = bundlerVersionInDependencies || bundlerVersionInDevDependencies; - const shouldThrowError = minSupportedVersion && this.isValidVersion(currentVersion) && this.isVersionLowerThan(currentVersion, minSupportedVersion); - if (shouldThrowError) { - this.$errors.failWithoutHelp(util.format(BundleValidatorMessages.NotSupportedVersion, minSupportedVersion)); - } + const currentVersion = bundlerVersionInDependencies || bundlerVersionInDevDependencies; + const shouldThrowError = minSupportedVersion && this.isValidVersion(currentVersion) && this.isVersionLowerThan(currentVersion, minSupportedVersion); + if (shouldThrowError) { + this.$errors.failWithoutHelp(util.format(BundleValidatorMessages.NotSupportedVersion, minSupportedVersion)); } } } diff --git a/lib/helpers/deploy-command-helper.ts b/lib/helpers/deploy-command-helper.ts index a5a0e9d652..d037ad435d 100644 --- a/lib/helpers/deploy-command-helper.ts +++ b/lib/helpers/deploy-command-helper.ts @@ -1,45 +1,84 @@ -export class DeployCommandHelper implements IDeployCommandHelper { +import { DeployController } from "../controllers/deploy-controller"; +import { BuildController } from "../controllers/build-controller"; - constructor(private $options: IOptions, - private $platformService: IPlatformService, - private $projectData: IProjectData) { - this.$projectData.initializeProjectData(); - } +export class DeployCommandHelper { + constructor( + private $buildController: BuildController, + private $devicesService: Mobile.IDevicesService, + private $deployController: DeployController, + private $options: IOptions, + private $projectData: IProjectData + ) { } - public getDeployPlatformInfo(platform: string): IDeployPlatformInfo { - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { - bundle: !!this.$options.bundle, - release: this.$options.release, - useHotModuleReload: this.$options.hmr - }; - const deployOptions: IDeployPlatformOptions = { - clean: this.$options.clean, - device: this.$options.device, + public async deploy(platform: string, additionalOptions?: ILiveSyncCommandHelperAdditionalOptions) { + const emulator = this.$options.emulator; + await this.$devicesService.initialize({ + deviceId: this.$options.device, + platform, + emulator, + skipInferPlatform: !platform, + sdk: this.$options.sdk + }); + + const devices = this.$devicesService.getDeviceInstances() + .filter(d => !platform || d.deviceInfo.platform.toLowerCase() === platform.toLowerCase()); + + const deviceDescriptors: ILiveSyncDeviceInfo[] = devices + .map(d => { + const buildConfig: IBuildConfig = { + buildForDevice: !d.isEmulator, + iCloudContainerEnvironment: this.$options.iCloudContainerEnvironment, + projectDir: this.$options.path, + clean: this.$options.clean, + teamId: this.$options.teamId, + device: this.$options.device, + provision: this.$options.provision, + release: this.$options.release, + keyStoreAlias: this.$options.keyStoreAlias, + keyStorePath: this.$options.keyStorePath, + keyStoreAliasPassword: this.$options.keyStoreAliasPassword, + keyStorePassword: this.$options.keyStorePassword + }; + + const buildAction = additionalOptions && additionalOptions.buildPlatform ? + additionalOptions.buildPlatform.bind(additionalOptions.buildPlatform, d.deviceInfo.platform, buildConfig, this.$projectData) : + this.$buildController.prepareAndBuild.bind(this.$buildController, d.deviceInfo.platform, buildConfig, this.$projectData); + + const outputPath = additionalOptions && additionalOptions.getOutputDirectory && additionalOptions.getOutputDirectory({ + platform: d.deviceInfo.platform, + emulator: d.isEmulator, + projectDir: this.$projectData.projectDir + }); + + const info: ILiveSyncDeviceInfo = { + identifier: d.deviceInfo.identifier, + buildAction, + debuggingEnabled: additionalOptions && additionalOptions.deviceDebugMap && additionalOptions.deviceDebugMap[d.deviceInfo.identifier], + debugOptions: this.$options, + outputPath, + skipNativePrepare: additionalOptions && additionalOptions.skipNativePrepare, + }; + + return info; + }); + + const liveSyncInfo: ILiveSyncInfo = { projectDir: this.$projectData.projectDir, - emulator: this.$options.emulator, + skipWatcher: !this.$options.watch, + clean: this.$options.clean, release: this.$options.release, - forceInstall: true, - provision: this.$options.provision, - teamId: this.$options.teamId, - keyStoreAlias: this.$options.keyStoreAlias, - keyStoreAliasPassword: this.$options.keyStoreAliasPassword, - keyStorePassword: this.$options.keyStorePassword, - keyStorePath: this.$options.keyStorePath - }; - - const deployPlatformInfo: IDeployPlatformInfo = { - platform, - appFilesUpdaterOptions, - deployOptions, - projectData: this.$projectData, - buildPlatform: this.$platformService.buildPlatform.bind(this.$platformService), - config: this.$options, env: this.$options.env, - + timeout: this.$options.timeout, + useHotModuleReload: this.$options.hmr, + force: this.$options.force, + emulator: this.$options.emulator }; - return deployPlatformInfo; + await this.$deployController.deploy({ + projectDir: this.$projectData.projectDir, + liveSyncInfo, + deviceDescriptors + }); } } - $injector.register("deployCommandHelper", DeployCommandHelper); diff --git a/lib/helpers/livesync-command-helper.ts b/lib/helpers/livesync-command-helper.ts index 8b5c648298..9c6a1dc7fd 100644 --- a/lib/helpers/livesync-command-helper.ts +++ b/lib/helpers/livesync-command-helper.ts @@ -1,43 +1,41 @@ -import { LiveSyncEvents } from "../constants"; +import { RunController } from "../controllers/run-controller"; +import { BuildController } from "../controllers/build-controller"; +import { BuildDataService } from "../services/build-data-service"; +import { DeployController } from "../controllers/deploy-controller"; +import { RunOnDeviceEvents } from "../constants"; +import { RunEmitter } from "../emitters/run-emitter"; export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { public static MIN_SUPPORTED_WEBPACK_VERSION_WITH_HMR = "0.17.0"; - constructor(private $platformService: IPlatformService, + constructor( + private $buildDataService: BuildDataService, private $projectData: IProjectData, private $options: IOptions, - private $liveSyncService: ILiveSyncService, + private $runController: RunController, + private $runEmitter: RunEmitter, + private $deployController: DeployController, private $iosDeviceOperations: IIOSDeviceOperations, private $mobileHelper: Mobile.IMobileHelper, private $devicesService: Mobile.IDevicesService, - private $platformsData: IPlatformsData, + private $injector: IInjector, + private $buildController: BuildController, private $analyticsService: IAnalyticsService, private $bundleValidatorHelper: IBundleValidatorHelper, private $errors: IErrors, private $iOSSimulatorLogProvider: Mobile.IiOSSimulatorLogProvider, - private $logger: ILogger, - private $cleanupService: ICleanupService) { - } + private $cleanupService: ICleanupService + ) { } - public getPlatformsForOperation(platform: string): string[] { - const availablePlatforms = platform ? [platform] : _.values(this.$platformsData.availablePlatforms); - return availablePlatforms; + private get $platformsDataService(): IPlatformsDataService { + return this.$injector.resolve("platformsDataService"); } - public async executeCommandLiveSync(platform?: string, additionalOptions?: ILiveSyncCommandHelperAdditionalOptions) { - if (additionalOptions && additionalOptions.syncToPreviewApp) { - return; - } - - if (!this.$options.syncAllFiles) { - this.$logger.info("Skipping node_modules folder! Use the syncAllFiles option to sync files from this folder."); - } - - const emulator = this.$options.emulator; + public async getDeviceInstances(platform?: string): Promise { await this.$devicesService.initialize({ - deviceId: this.$options.device, platform, - emulator, + deviceId: this.$options.device, + emulator: this.$options.emulator, skipInferPlatform: !platform, sdk: this.$options.sdk }); @@ -45,10 +43,26 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { const devices = this.$devicesService.getDeviceInstances() .filter(d => !platform || d.deviceInfo.platform.toLowerCase() === platform.toLowerCase()); - await this.executeLiveSyncOperation(devices, platform, additionalOptions); + return devices; } - public async executeLiveSyncOperation(devices: Mobile.IDevice[], platform: string, additionalOptions?: ILiveSyncCommandHelperAdditionalOptions): Promise { + public createLiveSyncInfo(): ILiveSyncInfo { + const liveSyncInfo: ILiveSyncInfo = { + projectDir: this.$projectData.projectDir, + skipWatcher: !this.$options.watch || this.$options.justlaunch, + clean: this.$options.clean, + release: this.$options.release, + env: this.$options.env, + timeout: this.$options.timeout, + useHotModuleReload: this.$options.hmr, + force: this.$options.force, + emulator: this.$options.emulator + }; + + return liveSyncInfo; + } + + public async createDeviceDescriptors(devices: Mobile.IDevice[], platform: string, additionalOptions?: ILiveSyncCommandHelperAdditionalOptions): Promise { if (!devices || !devices.length) { if (platform) { this.$errors.failWithoutHelp("Unable to find applicable devices to execute operation. Ensure connected devices are trusted and try again."); @@ -69,16 +83,9 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { } } - if (this.$options.release) { - await this.runInReleaseMode(platform, additionalOptions); - return; - } - // Now let's take data for each device: const deviceDescriptors: ILiveSyncDeviceInfo[] = devices .map(d => { - let buildAction: IBuildAction; - const buildConfig: IBuildConfig = { buildForDevice: !d.isEmulator, iCloudContainerEnvironment: this.$options.iCloudContainerEnvironment, @@ -94,50 +101,95 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { keyStorePassword: this.$options.keyStorePassword }; - buildAction = additionalOptions && additionalOptions.buildPlatform ? + const buildData = this.$buildDataService.getBuildData(this.$projectData.projectDir, d.deviceInfo.platform, buildConfig); + + const buildAction = additionalOptions && additionalOptions.buildPlatform ? additionalOptions.buildPlatform.bind(additionalOptions.buildPlatform, d.deviceInfo.platform, buildConfig, this.$projectData) : - this.$platformService.buildPlatform.bind(this.$platformService, d.deviceInfo.platform, buildConfig, this.$projectData); + this.$buildController.prepareAndBuild.bind(this.$buildController, buildData); + + const outputPath = additionalOptions && additionalOptions.getOutputDirectory && additionalOptions.getOutputDirectory({ + platform: d.deviceInfo.platform, + emulator: d.isEmulator, + projectDir: this.$projectData.projectDir + }); const info: ILiveSyncDeviceInfo = { identifier: d.deviceInfo.identifier, - platformSpecificOptions: this.$options, buildAction, - debugggingEnabled: additionalOptions && additionalOptions.deviceDebugMap && additionalOptions.deviceDebugMap[d.deviceInfo.identifier], + debuggingEnabled: additionalOptions && additionalOptions.deviceDebugMap && additionalOptions.deviceDebugMap[d.deviceInfo.identifier], debugOptions: this.$options, - outputPath: additionalOptions && additionalOptions.getOutputDirectory && additionalOptions.getOutputDirectory({ - platform: d.deviceInfo.platform, - emulator: d.isEmulator, - projectDir: this.$projectData.projectDir - }), + outputPath, skipNativePrepare: additionalOptions && additionalOptions.skipNativePrepare, }; return info; }); + return deviceDescriptors; + } + + public getPlatformsForOperation(platform: string): string[] { + const availablePlatforms = platform ? [platform] : _.values(this.$mobileHelper.platformNames.map(p => p.toLowerCase())); + return availablePlatforms; + } + + public async executeCommandLiveSync(platform?: string, additionalOptions?: ILiveSyncCommandHelperAdditionalOptions) { + const devices = await this.getDeviceInstances(platform); + await this.executeLiveSyncOperation(devices, platform, additionalOptions); + } + + public async executeLiveSyncOperation(devices: Mobile.IDevice[], platform: string, additionalOptions?: ILiveSyncCommandHelperAdditionalOptions): Promise { + const deviceDescriptors = await this.createDeviceDescriptors(devices, platform, additionalOptions); + const liveSyncInfo: ILiveSyncInfo = { projectDir: this.$projectData.projectDir, - skipWatcher: !this.$options.watch, - watchAllFiles: this.$options.syncAllFiles, + skipWatcher: !this.$options.watch || this.$options.justlaunch, clean: this.$options.clean, - bundle: !!this.$options.bundle, release: this.$options.release, env: this.$options.env, timeout: this.$options.timeout, useHotModuleReload: this.$options.hmr, - force: this.$options.force + force: this.$options.force, + emulator: this.$options.emulator }; + if (this.$options.release) { + await this.$deployController.deploy({ + projectDir: this.$projectData.projectDir, + liveSyncInfo: { ...liveSyncInfo, clean: true, skipWatcher: true }, + deviceDescriptors + }); + + await this.$devicesService.initialize({ + platform, + deviceId: this.$options.device, + emulator: this.$options.emulator, + skipInferPlatform: !platform, + sdk: this.$options.sdk + }); + + for (const deviceDescriptor of deviceDescriptors) { + const device = this.$devicesService.getDeviceByIdentifier(deviceDescriptor.identifier); + await device.applicationManager.startApplication({ appId: this.$projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()], projectName: this.$projectData.projectName }); + } + + return; + } + + await this.$runController.run({ + projectDir: this.$projectData.projectDir, + liveSyncInfo, + deviceDescriptors + }); + const remainingDevicesToSync = devices.map(d => d.deviceInfo.identifier); - this.$liveSyncService.on(LiveSyncEvents.liveSyncStopped, (data: { projectDir: string, deviceIdentifier: string }) => { + this.$runEmitter.on(RunOnDeviceEvents.runOnDeviceStopped, (data: { projectDir: string, deviceIdentifier: string }) => { _.remove(remainingDevicesToSync, d => d === data.deviceIdentifier); if (remainingDevicesToSync.length === 0) { process.exit(ErrorCodes.ALL_DEVICES_DISCONNECTED); } }); - - await this.$liveSyncService.liveSync(deviceDescriptors, liveSyncInfo); } public async validatePlatform(platform: string): Promise> { @@ -145,7 +197,7 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { const availablePlatforms = this.getPlatformsForOperation(platform); for (const availablePlatform of availablePlatforms) { - const platformData = this.$platformsData.getPlatformData(availablePlatform, this.$projectData); + const platformData = this.$platformsDataService.getPlatformData(availablePlatform, this.$projectData); const platformProjectService = platformData.platformProjectService; const validateOutput = await platformProjectService.validate(this.$projectData, this.$options); result[availablePlatform.toLowerCase()] = validateOutput; @@ -156,47 +208,6 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { return result; } - - private async runInReleaseMode(platform: string, additionalOptions?: ILiveSyncCommandHelperAdditionalOptions): Promise { - const runPlatformOptions: IRunPlatformOptions = { - device: this.$options.device, - emulator: this.$options.emulator, - justlaunch: this.$options.justlaunch - }; - - const deployOptions = _.merge(({ - projectDir: this.$projectData.projectDir, - clean: true - }), this.$options.argv); - - const availablePlatforms = this.getPlatformsForOperation(platform); - for (const currentPlatform of availablePlatforms) { - const deployPlatformInfo: IDeployPlatformInfo = { - platform: currentPlatform, - appFilesUpdaterOptions: { - bundle: !!this.$options.bundle, - release: this.$options.release, - useHotModuleReload: this.$options.hmr - }, - deployOptions, - buildPlatform: this.$platformService.buildPlatform.bind(this.$platformService), - projectData: this.$projectData, - config: this.$options, - env: this.$options.env - }; - - await this.$platformService.deployPlatform(deployPlatformInfo); - - await this.$platformService.startApplication( - currentPlatform, - runPlatformOptions, - { - appId: this.$projectData.projectIdentifiers[currentPlatform.toLowerCase()], - projectName: this.$projectData.projectName - } - ); - } - } } $injector.register("liveSyncCommandHelper", LiveSyncCommandHelper); diff --git a/lib/helpers/platform-command-helper.ts b/lib/helpers/platform-command-helper.ts new file mode 100644 index 0000000000..6b76864bf4 --- /dev/null +++ b/lib/helpers/platform-command-helper.ts @@ -0,0 +1,190 @@ +import * as path from "path"; +import * as semver from "semver"; +import * as temp from "temp"; +import * as constants from "../constants"; +import { PlatformController } from "../controllers/platform-controller"; +import { PlatformValidationService } from "../services/platform/platform-validation-service"; + +export class PlatformCommandHelper implements IPlatformCommandHelper { + constructor( + private $platformController: PlatformController, + private $fs: IFileSystem, + private $errors: IErrors, + private $logger: ILogger, + private $mobileHelper: Mobile.IMobileHelper, + private $packageInstallationManager: IPackageInstallationManager, + private $pacoteService: IPacoteService, + private $platformsDataService: IPlatformsDataService, + private $platformValidationService: PlatformValidationService, + private $projectChangesService: IProjectChangesService, + private $projectDataService: IProjectDataService + ) { } + + public async addPlatforms(platforms: string[], projectData: IProjectData, frameworkPath: string): Promise { + const platformsDir = projectData.platformsDir; + this.$fs.ensureDirectoryExists(platformsDir); + + for (const platform of platforms) { + this.$platformValidationService.validatePlatform(platform, projectData); + const platformPath = path.join(projectData.platformsDir, platform); + + const isPlatformAdded = this.isPlatformAdded(platform, platformPath, projectData); + if (isPlatformAdded) { + this.$errors.failWithoutHelp(`Platform ${platform} already added`); + } + + await this.$platformController.addPlatform({ + projectDir: projectData.projectDir, + platform, + frameworkPath, + }); + } + } + + public async cleanPlatforms(platforms: string[], projectData: IProjectData, framworkPath: string): Promise { + for (const platform of platforms) { + await this.removePlatforms([platform], projectData); + + const version: string = this.getCurrentPlatformVersion(platform, projectData); + const platformParam = version ? `${platform}@${version}` : platform; + await this.addPlatforms([platformParam], projectData, framworkPath); + } + } + + public async removePlatforms(platforms: string[], projectData: IProjectData): Promise { + for (const platform of platforms) { + this.$platformValidationService.validatePlatformInstalled(platform, projectData); + const platformData = this.$platformsDataService.getPlatformData(platform, projectData); + let errorMessage; + + try { + await platformData.platformProjectService.stopServices(platformData.projectRoot); + } catch (err) { + errorMessage = err.message; + } + + try { + const platformDir = path.join(projectData.platformsDir, platform.toLowerCase()); + this.$fs.deleteDirectory(platformDir); + this.$projectDataService.removeNSProperty(projectData.projectDir, platformData.frameworkPackageName); + + this.$logger.out(`Platform ${platform} successfully removed.`); + } catch (err) { + this.$logger.error(`Failed to remove ${platform} platform with errors:`); + if (errorMessage) { + this.$logger.error(errorMessage); + } + this.$errors.failWithoutHelp(err.message); + } + } + } + + public async updatePlatforms(platforms: string[], projectData: IProjectData): Promise { + for (const platformParam of platforms) { + const data = platformParam.split("@"), + platform = data[0], + version = data[1]; + + const hasPlatformDirectory = this.$fs.exists(path.join(projectData.platformsDir, platform.toLowerCase())); + if (hasPlatformDirectory) { + await this.updatePlatform(platform, version, projectData); + } else { + await this.$platformController.addPlatform({ + projectDir: projectData.projectDir, + platform: platformParam, + }); + } + } + } + + public getInstalledPlatforms(projectData: IProjectData): string[] { + if (!this.$fs.exists(projectData.platformsDir)) { + return []; + } + + const subDirs = this.$fs.readDirectory(projectData.platformsDir); + return _.filter(subDirs, p => this.$mobileHelper.platformNames.indexOf(p) > -1); + } + + public getAvailablePlatforms(projectData: IProjectData): string[] { + const installedPlatforms = this.getInstalledPlatforms(projectData); + return _.filter(this.$mobileHelper.platformNames, p => { + return installedPlatforms.indexOf(p) < 0 && this.$platformValidationService.isPlatformSupportedForOS(p, projectData); // Only those not already installed + }); + } + + public getPreparedPlatforms(projectData: IProjectData): string[] { + return _.filter(this.$mobileHelper.platformNames, p => { return this.isPlatformPrepared(p, projectData); }); + } + + public getCurrentPlatformVersion(platform: string, projectData: IProjectData): string { + const platformData = this.$platformsDataService.getPlatformData(platform, projectData); + const currentPlatformData: any = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName); + const version = currentPlatformData && currentPlatformData.version; + + return version; + } + + private isPlatformAdded(platform: string, platformPath: string, projectData: IProjectData): boolean { + if (!this.$fs.exists(platformPath)) { + return false; + } + + const platformData = this.$platformsDataService.getPlatformData(platform, projectData); + const prepareInfo = this.$projectChangesService.getPrepareInfo(platformData); + if (!prepareInfo) { + return true; + } + + return prepareInfo.nativePlatformStatus !== constants.NativePlatformStatus.requiresPlatformAdd; + } + + private async updatePlatform(platform: string, version: string, projectData: IProjectData): Promise { + const platformData = this.$platformsDataService.getPlatformData(platform, projectData); + + const data = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName); + const currentVersion = data && data.version ? data.version : "0.2.0"; + + const installedModuleDir = temp.mkdirSync("runtime-to-update"); + let newVersion = version === constants.PackageVersion.NEXT ? + await this.$packageInstallationManager.getNextVersion(platformData.frameworkPackageName) : + version || await this.$packageInstallationManager.getLatestCompatibleVersion(platformData.frameworkPackageName); + await this.$pacoteService.extractPackage(`${platformData.frameworkPackageName}@${newVersion}`, installedModuleDir); + const cachedPackageData = this.$fs.readJson(path.join(installedModuleDir, "package.json")); + newVersion = (cachedPackageData && cachedPackageData.version) || newVersion; + + const canUpdate = platformData.platformProjectService.canUpdatePlatform(installedModuleDir, projectData); + if (canUpdate) { + if (!semver.valid(newVersion)) { + this.$errors.fail("The version %s is not valid. The version should consists from 3 parts separated by dot.", newVersion); + } + + if (!semver.gt(currentVersion, newVersion)) { + await this.updatePlatformCore(platformData, { currentVersion, newVersion, canUpdate }, projectData); + } else if (semver.eq(currentVersion, newVersion)) { + this.$errors.fail("Current and new version are the same."); + } else { + this.$errors.fail(`Your current version: ${currentVersion} is higher than the one you're trying to install ${newVersion}.`); + } + } else { + this.$errors.failWithoutHelp("Native Platform cannot be updated."); + } + } + + private async updatePlatformCore(platformData: IPlatformData, updateOptions: IUpdatePlatformOptions, projectData: IProjectData): Promise { + let packageName = platformData.normalizedPlatformName.toLowerCase(); + await this.removePlatforms([packageName], projectData); + packageName = updateOptions.newVersion ? `${packageName}@${updateOptions.newVersion}` : packageName; + await this.$platformController.addPlatform({ + projectDir: projectData.projectDir, + platform: packageName + }); + this.$logger.out("Successfully updated to version ", updateOptions.newVersion); + } + + private isPlatformPrepared(platform: string, projectData: IProjectData): boolean { + const platformData = this.$platformsDataService.getPlatformData(platform, projectData); + return platformData.platformProjectService.isPlatformPrepared(platformData.projectRoot, projectData); + } +} +$injector.register("platformCommandHelper", PlatformCommandHelper); diff --git a/lib/options.ts b/lib/options.ts index ddc03b0459..c63e9b8909 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -105,7 +105,6 @@ export class Options { bundle: { type: OptionType.String, hasSensitiveValue: false }, all: { type: OptionType.Boolean, hasSensitiveValue: false }, teamId: { type: OptionType.Object, hasSensitiveValue: true }, - syncAllFiles: { type: OptionType.Boolean, default: false, hasSensitiveValue: false }, chrome: { type: OptionType.Boolean, hasSensitiveValue: false }, inspector: { type: OptionType.Boolean, hasSensitiveValue: false }, clean: { type: OptionType.Boolean, hasSensitiveValue: false }, diff --git a/lib/platform-command-param.ts b/lib/platform-command-param.ts index b6ec67eb6b..b53ad9979c 100644 --- a/lib/platform-command-param.ts +++ b/lib/platform-command-param.ts @@ -1,10 +1,10 @@ export class PlatformCommandParameter implements ICommandParameter { - constructor(private $platformService: IPlatformService, + constructor(private $platformValidationService: IPlatformValidationService, private $projectData: IProjectData) { } mandatory = true; async validate(value: string): Promise { this.$projectData.initializeProjectData(); - this.$platformService.validatePlatform(value, this.$projectData); + this.$platformValidationService.validatePlatform(value, this.$projectData); return true; } } diff --git a/lib/platforms-data.ts b/lib/platforms-data.ts deleted file mode 100644 index 7af6a383f1..0000000000 --- a/lib/platforms-data.ts +++ /dev/null @@ -1,34 +0,0 @@ -export class PlatformsData implements IPlatformsData { - private platformsData: { [index: string]: any } = {}; - - constructor($androidProjectService: IPlatformProjectService, - $iOSProjectService: IPlatformProjectService) { - - this.platformsData = { - ios: $iOSProjectService, - android: $androidProjectService - }; - } - - public get platformsNames() { - return Object.keys(this.platformsData); - } - - public getPlatformData(platform: string, projectData: IProjectData): IPlatformData { - const platformKey = platform && _.first(platform.toLowerCase().split("@")); - let platformData: IPlatformData; - if (platformKey) { - platformData = this.platformsData[platformKey] && this.platformsData[platformKey].getPlatformData(projectData); - } - - return platformData; - } - - public get availablePlatforms(): any { - return { - iOS: "ios", - Android: "android" - }; - } -} -$injector.register("platformsData", PlatformsData); diff --git a/lib/providers/project-files-provider.ts b/lib/providers/project-files-provider.ts index 7451b075ac..d72cf7bd42 100644 --- a/lib/providers/project-files-provider.ts +++ b/lib/providers/project-files-provider.ts @@ -4,7 +4,7 @@ import * as path from "path"; import { ProjectFilesProviderBase } from "../common/services/project-files-provider-base"; export class ProjectFilesProvider extends ProjectFilesProviderBase { - constructor(private $platformsData: IPlatformsData, + constructor(private $platformsDataService: IPlatformsDataService, $mobileHelper: Mobile.IMobileHelper, $options: IOptions) { super($mobileHelper, $options); @@ -13,7 +13,7 @@ export class ProjectFilesProvider extends ProjectFilesProviderBase { private static INTERNAL_NONPROJECT_FILES = ["**/*.ts"]; public mapFilePath(filePath: string, platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): string { - const platformData = this.$platformsData.getPlatformData(platform.toLowerCase(), projectData); + const platformData = this.$platformsDataService.getPlatformData(platform.toLowerCase(), projectData); const parsedFilePath = this.getPreparedFilePath(filePath, projectFilesConfig); let mappedFilePath = ""; let relativePath; diff --git a/lib/resolvers/livesync-service-resolver.ts b/lib/resolvers/livesync-service-resolver.ts new file mode 100644 index 0000000000..5d97b1d2c9 --- /dev/null +++ b/lib/resolvers/livesync-service-resolver.ts @@ -0,0 +1,18 @@ +export class LiveSyncServiceResolver { + constructor( + private $errors: IErrors, + private $injector: IInjector, + private $mobileHelper: Mobile.IMobileHelper + ) { } + + public resolveLiveSyncService(platform: string): IPlatformLiveSyncService { + if (this.$mobileHelper.isiOSPlatform(platform)) { + return this.$injector.resolve("iOSLiveSyncService"); + } else if (this.$mobileHelper.isAndroidPlatform(platform)) { + return this.$injector.resolve("androidLiveSyncService"); + } + + this.$errors.failWithoutHelp(`Invalid platform ${platform}. Supported platforms are: ${this.$mobileHelper.platformNames.join(", ")}`); + } +} +$injector.register("liveSyncServiceResolver", LiveSyncServiceResolver); diff --git a/lib/services/android-plugin-build-service.ts b/lib/services/android-plugin-build-service.ts index d2810a6db0..64ea547406 100644 --- a/lib/services/android-plugin-build-service.ts +++ b/lib/services/android-plugin-build-service.ts @@ -4,11 +4,11 @@ import { getShortPluginName, hook } from "../common/helpers"; import { Builder, parseString } from "xml2js"; export class AndroidPluginBuildService implements IAndroidPluginBuildService { - private get $platformService(): IPlatformService { - return this.$injector.resolve("platformService"); + private get $platformsDataService(): IPlatformsDataService { + return this.$injector.resolve("platformsDataService"); } - constructor(private $injector: IInjector, + constructor( private $fs: IFileSystem, private $childProcess: IChildProcess, private $hostInfo: IHostInfo, @@ -19,7 +19,9 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $errors: IErrors, private $filesHashService: IFilesHashService, - public $hooksService: IHooksService) { } + public $hooksService: IHooksService, + private $injector: IInjector + ) { } private static MANIFEST_ROOT = { $: { @@ -290,10 +292,9 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { private async getRuntimeGradleVersions(projectDir: string): Promise { let runtimeGradleVersions: IRuntimeGradleVersions = null; if (projectDir) { - const projectRuntimeVersion = this.$platformService.getCurrentPlatformVersion( - this.$devicePlatformsConstants.Android, - this.$projectDataService.getProjectData(projectDir)); - runtimeGradleVersions = await this.getGradleVersions(projectRuntimeVersion); + const projectData = this.$projectDataService.getProjectData(projectDir); + const platformData = this.$platformsDataService.getPlatformData(this.$devicePlatformsConstants.Android, projectData); + const projectRuntimeVersion = platformData.platformProjectService.getFrameworkVersion(projectData); this.$logger.trace(`Got gradle versions ${JSON.stringify(runtimeGradleVersions)} from runtime v${projectRuntimeVersion}`); } diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index a73447b5a1..56604202c9 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -7,7 +7,7 @@ import { DeviceAndroidDebugBridge } from "../common/mobile/android/device-androi import { Configurations, LiveSyncPaths } from "../common/constants"; import { performanceLog } from ".././common/decorators"; -export class AndroidProjectService extends projectServiceBaseLib.PlatformProjectServiceBase implements IPlatformProjectService { +export class AndroidProjectService extends projectServiceBaseLib.PlatformProjectServiceBase { private static VALUES_DIRNAME = "values"; private static VALUES_VERSION_DIRNAME_PREFIX = AndroidProjectService.VALUES_DIRNAME + "-v"; private static ANDROID_PLATFORM_NAME = "android"; @@ -46,11 +46,17 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject this._platformData = { frameworkPackageName: constants.TNS_ANDROID_RUNTIME_NAME, normalizedPlatformName: "Android", + platformNameLowerCase: "android", appDestinationDirectoryPath: path.join(...appDestinationDirectoryArr), - platformProjectService: this, + platformProjectService: this, projectRoot: projectRoot, - getBuildOutputPath: () => path.join(...deviceBuildOutputArr), - bundleBuildOutputPath: path.join(projectRoot, constants.APP_FOLDER_NAME, constants.BUILD_DIR, constants.OUTPUTS_DIR, constants.BUNDLE_DIR), + getBuildOutputPath: (buildOptions: IBuildOutputOptions) => { + if (buildOptions.androidBundle) { + return path.join(projectRoot, constants.APP_FOLDER_NAME, constants.BUILD_DIR, constants.OUTPUTS_DIR, constants.BUNDLE_DIR); + } + + return path.join(...deviceBuildOutputArr); + }, getValidBuildOutputData: (buildOptions: IBuildOutputOptions): IValidBuildOutputData => { const buildMode = buildOptions.release ? Configurations.Release.toLowerCase() : Configurations.Debug.toLowerCase(); @@ -123,7 +129,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject }; } - public async createProject(frameworkDir: string, frameworkVersion: string, projectData: IProjectData, config: ICreateProjectOptions): Promise { + public async createProject(frameworkDir: string, frameworkVersion: string, projectData: IProjectData): Promise { if (semver.lt(frameworkVersion, AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE)) { this.$errors.failWithoutHelp(`The NativeScript CLI requires Android runtime ${AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE} or later to work properly.`); } @@ -160,9 +166,9 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject _.map(directoriesToClean, dir => this.$fs.deleteDirectory(dir)); } - public async interpolateData(projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise { + public async interpolateData(projectData: IProjectData): Promise { // Interpolate the apilevel and package - this.interpolateConfigurationFile(projectData, platformSpecificData); + this.interpolateConfigurationFile(projectData); const appResourcesDirectoryPath = projectData.getAppResourcesDirectoryPath(); let stringsFilePath: string; @@ -192,13 +198,9 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } } - public interpolateConfigurationFile(projectData: IProjectData, platformSpecificData: IPlatformSpecificData): void { + public interpolateConfigurationFile(projectData: IProjectData): void { const manifestPath = this.getPlatformData(projectData).configurationFilePath; shell.sed('-i', /__PACKAGE__/, projectData.projectIdentifiers.android, manifestPath); - if (this.$androidToolsInfo.getToolsInfo().androidHomeEnvVar) { - const sdk = (platformSpecificData && platformSpecificData.sdk) || (this.$androidToolsInfo.getToolsInfo().compileSdkVersion || "").toString(); - shell.sed('-i', /__APILEVEL__/, sdk, manifestPath); - } } private getProjectNameFromId(projectData: IProjectData): string { @@ -235,7 +237,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject const platformData = this.getPlatformData(projectData); await this.$gradleBuildService.buildProject(platformData.projectRoot, buildConfig); - const outputPath = buildConfig.androidBundle ? platformData.bundleBuildOutputPath : platformData.getBuildOutputPath(buildConfig); + const outputPath = platformData.getBuildOutputPath(buildConfig); await this.$filesHashService.saveHashesForProject(this._platformData, outputPath); } @@ -368,7 +370,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject await adb.executeShellCommand(["rm", "-rf", deviceRootPath]); } - public async checkForChanges(changesInfo: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): Promise { + public async checkForChanges(): Promise { // Nothing android specific to check yet. } diff --git a/lib/services/app-files-updater.ts b/lib/services/app-files-updater.ts deleted file mode 100644 index 0e14011477..0000000000 --- a/lib/services/app-files-updater.ts +++ /dev/null @@ -1,104 +0,0 @@ -import * as path from "path"; -import * as minimatch from "minimatch"; -import * as constants from "../constants"; -// TODO: ?? -import * as fs from "fs"; - -export class AppFilesUpdater { - constructor(private appSourceDirectoryPath: string, - private appDestinationDirectoryPath: string, - public options: IAppFilesUpdaterOptions, - public fileSystem: IFileSystem - ) { - } - - public updateApp(updateAppOptions: IUpdateAppOptions, projectData: IProjectData): void { - this.cleanDestinationApp(updateAppOptions); - let sourceFiles = updateAppOptions.filesToSync || this.resolveAppSourceFiles(projectData); - - // exclude the app_resources directory from being enumerated - // for copying if it is present in the application sources dir - const appResourcesPathNormalized = path.normalize(projectData.appResourcesDirectoryPath + path.sep); - sourceFiles = sourceFiles.filter(dirName => !path.normalize(dirName).startsWith(appResourcesPathNormalized)); - - updateAppOptions.beforeCopyAction(sourceFiles); - this.copyAppSourceFiles(sourceFiles); - } - - public cleanDestinationApp(updateAppOptions?: IUpdateAppOptions): void { - let itemsToRemove: string[]; - - if (updateAppOptions && updateAppOptions.filesToRemove) { - // We get here during LiveSync - we only want to get rid of files, that the file system watcher detected were deleted - itemsToRemove = updateAppOptions.filesToRemove.map(fileToRemove => path.relative(this.appSourceDirectoryPath, fileToRemove)); - } else { - // We get here during the initial sync before the file system watcher is even started - // delete everything and prepare everything anew just to be sure - // Delete the destination app in order to prevent EEXIST errors when symlinks are used. - itemsToRemove = this.readDestinationDir(); - itemsToRemove = itemsToRemove.filter( - (directoryName: string) => directoryName !== constants.TNS_MODULES_FOLDER_NAME); - - } - - _(itemsToRemove).each((directoryItem: string) => { - this.deleteDestinationItem(directoryItem); - }); - } - - protected readDestinationDir(): string[] { - if (this.fileSystem.exists(this.appDestinationDirectoryPath)) { - return this.fileSystem.readDirectory(this.appDestinationDirectoryPath); - } else { - return []; - } - } - - protected deleteDestinationItem(directoryItem: string): void { - this.fileSystem.deleteDirectory(path.join(this.appDestinationDirectoryPath, directoryItem)); - } - - protected readSourceDir(projectData: IProjectData): string[] { - const tnsDir = path.join(this.appSourceDirectoryPath, constants.TNS_MODULES_FOLDER_NAME); - - return this.fileSystem.enumerateFilesInDirectorySync(this.appSourceDirectoryPath, null, { includeEmptyDirectories: true }).filter(dirName => dirName !== tnsDir); - } - - protected resolveAppSourceFiles(projectData: IProjectData): string[] { - if (this.options.bundle) { - return []; - } - - // Copy all files from app dir, but make sure to exclude tns_modules and application resources - let sourceFiles = this.readSourceDir(projectData); - - if (this.options.release) { - const testsFolderPath = path.join(this.appSourceDirectoryPath, 'tests'); - sourceFiles = sourceFiles.filter(source => source.indexOf(testsFolderPath) === -1); - } - - // Remove .ts and .js.map files in release - if (this.options.release) { - constants.LIVESYNC_EXCLUDED_FILE_PATTERNS.forEach(pattern => sourceFiles = sourceFiles.filter(file => !minimatch(file, pattern, { nocase: true }))); - } - - return sourceFiles; - } - - protected copyAppSourceFiles(sourceFiles: string[]): void { - sourceFiles.map(source => { - const destinationPath = path.join(this.appDestinationDirectoryPath, path.relative(this.appSourceDirectoryPath, source)); - - let exists = fs.lstatSync(source); - if (exists.isSymbolicLink()) { - source = fs.realpathSync(source); - exists = fs.lstatSync(source); - } - if (exists.isDirectory()) { - return this.fileSystem.createDirectory(destinationPath); - } - - return this.fileSystem.copyFile(source, destinationPath); - }); - } -} diff --git a/lib/services/build-artefacts-service.ts b/lib/services/build-artefacts-service.ts new file mode 100644 index 0000000000..5a62f83c76 --- /dev/null +++ b/lib/services/build-artefacts-service.ts @@ -0,0 +1,95 @@ +import * as path from "path"; + +export class BuildArtefactsService implements IBuildArtefactsService { + constructor( + private $errors: IErrors, + private $fs: IFileSystem, + private $logger: ILogger + ) { } + + public async getLatestApplicationPackagePath(platformData: IPlatformData, buildOutputOptions: IBuildOutputOptions): Promise { + const outputPath = buildOutputOptions.outputPath || platformData.getBuildOutputPath(buildOutputOptions); + const applicationPackage = this.getLatestApplicationPackage(outputPath, platformData.getValidBuildOutputData(buildOutputOptions)); + const packageFile = applicationPackage.packageName; + + if (!packageFile || !this.$fs.exists(packageFile)) { + this.$errors.failWithoutHelp(`Unable to find built application. Try 'tns build ${platformData.platformNameLowerCase}'.`); + } + + return packageFile; + } + + public getAllApplicationPackages(buildOutputPath: string, validBuildOutputData: IValidBuildOutputData): IApplicationPackage[] { + const rootFiles = this.$fs.readDirectory(buildOutputPath).map(filename => path.join(buildOutputPath, filename)); + let result = this.getApplicationPackagesCore(rootFiles, validBuildOutputData.packageNames); + if (result) { + return result; + } + + const candidates = this.$fs.enumerateFilesInDirectorySync(buildOutputPath); + result = this.getApplicationPackagesCore(candidates, validBuildOutputData.packageNames); + if (result) { + return result; + } + + if (validBuildOutputData.regexes && validBuildOutputData.regexes.length) { + const packages = candidates.filter(filepath => _.some(validBuildOutputData.regexes, regex => regex.test(path.basename(filepath)))); + return this.createApplicationPackages(packages); + } + + return []; + } + + public copyLastOutput(targetPath: string, platformData: IPlatformData, buildOutputOptions: IBuildOutputOptions): void { + targetPath = path.resolve(targetPath); + + const outputPath = buildOutputOptions.outputPath || platformData.getBuildOutputPath(buildOutputOptions); + const applicationPackage = this.getLatestApplicationPackage(outputPath, platformData.getValidBuildOutputData(buildOutputOptions)); + const packageFile = applicationPackage.packageName; + + this.$fs.ensureDirectoryExists(path.dirname(targetPath)); + + if (this.$fs.exists(targetPath) && this.$fs.getFsStats(targetPath).isDirectory()) { + const sourceFileName = path.basename(packageFile); + this.$logger.trace(`Specified target path: '${targetPath}' is directory. Same filename will be used: '${sourceFileName}'.`); + targetPath = path.join(targetPath, sourceFileName); + } + this.$fs.copyFile(packageFile, targetPath); + this.$logger.info(`Copied file '${packageFile}' to '${targetPath}'.`); + } + + private getLatestApplicationPackage(buildOutputPath: string, validBuildOutputData: IValidBuildOutputData): IApplicationPackage { + let packages = this.getAllApplicationPackages(buildOutputPath, validBuildOutputData); + const packageExtName = path.extname(validBuildOutputData.packageNames[0]); + if (packages.length === 0) { + this.$errors.fail(`No ${packageExtName} found in ${buildOutputPath} directory.`); + } + + if (packages.length > 1) { + this.$logger.warn(`More than one ${packageExtName} found in ${buildOutputPath} directory. Using the last one produced from build.`); + } + + packages = _.sortBy(packages, pkg => pkg.time).reverse(); // We need to reverse because sortBy always sorts in ascending order + + return packages[0]; + } + + private getApplicationPackagesCore(candidates: string[], validPackageNames: string[]): IApplicationPackage[] { + const packages = candidates.filter(filePath => _.includes(validPackageNames, path.basename(filePath))); + if (packages.length > 0) { + return this.createApplicationPackages(packages); + } + + return null; + } + + private createApplicationPackages(packages: string[]): IApplicationPackage[] { + return packages.map(packageName => { + return { + packageName, + time: this.$fs.getFsStats(packageName).mtime + }; + }); + } +} +$injector.register("buildArtefactsService", BuildArtefactsService); diff --git a/lib/services/build-data-service.ts b/lib/services/build-data-service.ts new file mode 100644 index 0000000000..2cb3ea930f --- /dev/null +++ b/lib/services/build-data-service.ts @@ -0,0 +1,14 @@ +import { AndroidBuildData, IOSBuildData } from "../data/build-data"; + +export class BuildDataService implements IBuildDataService { + constructor(private $mobileHelper: Mobile.IMobileHelper) { } + + public getBuildData(projectDir: string, platform: string, data: any) { + if (this.$mobileHelper.isiOSPlatform(platform)) { + return new IOSBuildData(projectDir, platform, data); + } else if (this.$mobileHelper.isAndroidPlatform(platform)) { + return new AndroidBuildData(projectDir, platform, data); + } + } +} +$injector.register("buildDataService", BuildDataService); diff --git a/lib/services/build-info-file-service.ts b/lib/services/build-info-file-service.ts new file mode 100644 index 0000000000..f23679b195 --- /dev/null +++ b/lib/services/build-info-file-service.ts @@ -0,0 +1,38 @@ +import * as path from "path"; + +const buildInfoFileName = ".nsbuildinfo"; + +export class BuildInfoFileService implements IBuildInfoFileService { + constructor( + private $fs: IFileSystem, + private $projectChangesService: IProjectChangesService + ) { } + + public saveBuildInfoFile(platformData: IPlatformData, buildInfoFileDirname: string): void { + const buildInfoFile = path.join(buildInfoFileDirname, buildInfoFileName); + + const prepareInfo = this.$projectChangesService.getPrepareInfo(platformData); + const buildInfo: IBuildInfo = { + prepareTime: prepareInfo.changesRequireBuildTime, + buildTime: new Date().toString() + }; + + this.$fs.writeJson(buildInfoFile, buildInfo); + } + + public getBuildInfoFromFile(platformData: IPlatformData, buildData: IBuildData): IBuildInfo { + const outputPath = buildData.outputPath || platformData.getBuildOutputPath(buildData); + const buildInfoFile = path.join(outputPath, buildInfoFileName); + if (this.$fs.exists(buildInfoFile)) { + try { + const buildInfo = this.$fs.readJson(buildInfoFile); + return buildInfo; + } catch (e) { + return null; + } + } + + return null; + } +} +$injector.register("buildInfoFileService", BuildInfoFileService); diff --git a/lib/services/debug-service.ts b/lib/services/debug-service.ts index 3958df4f79..a52cecce2a 100644 --- a/lib/services/debug-service.ts +++ b/lib/services/debug-service.ts @@ -49,7 +49,7 @@ export class DebugService extends EventEmitter implements IDebugService { // TODO: Consider to move this code to ios-device-debug-service if (this.$mobileHelper.isiOSPlatform(device.deviceInfo.platform)) { - if (device.isEmulator && !debugData.pathToAppPackage && debugOptions.debugBrk) { + if (device.isEmulator && debugOptions.debugBrk) { this.$errors.failWithoutHelp("To debug on iOS simulator you need to provide path to the app package."); } diff --git a/lib/services/device/device-debug-app-service.ts b/lib/services/device/device-debug-app-service.ts new file mode 100644 index 0000000000..2b1f767f14 --- /dev/null +++ b/lib/services/device/device-debug-app-service.ts @@ -0,0 +1,102 @@ +import { performanceLog } from "../../common/decorators"; +import { RunEmitter } from "../../emitters/run-emitter"; +import { EOL } from "os"; + +export class DeviceDebugAppService { + constructor( + private $debugDataService: IDebugDataService, + private $debugService: IDebugService, + private $devicesService: Mobile.IDevicesService, + private $errors: IErrors, + private $logger: ILogger, + private $projectDataService: IProjectDataService, + private $runEmitter: RunEmitter + ) { } + + @performanceLog() + public async enableDebugging(projectData: IProjectData, deviceDescriptor: ILiveSyncDeviceInfo, refreshInfo: IRestartApplicationInfo): Promise { + const { debugOptions } = deviceDescriptor; + // we do not stop the application when debugBrk is false, so we need to attach, instead of launch + // if we try to send the launch request, the debugger port will not be printed and the command will timeout + debugOptions.start = !debugOptions.debugBrk; + + debugOptions.forceDebuggerAttachedEvent = refreshInfo.didRestart; + const deviceOption = { + deviceIdentifier: deviceDescriptor.identifier, + debugOptions: debugOptions, + }; + + return this.enableDebuggingCoreWithoutWaitingCurrentAction(deviceOption, deviceDescriptor, { projectDir: projectData.projectDir }); + } + + public async attachDebugger(settings: IAttachDebuggerOptions): Promise { + // Default values + if (settings.debugOptions) { + settings.debugOptions.chrome = settings.debugOptions.chrome === undefined ? true : settings.debugOptions.chrome; + settings.debugOptions.start = settings.debugOptions.start === undefined ? true : settings.debugOptions.start; + } else { + settings.debugOptions = { + chrome: true, + start: true + }; + } + + const projectData = this.$projectDataService.getProjectData(settings.projectDir); + const debugData = this.$debugDataService.createDebugData(projectData, { device: settings.deviceIdentifier }); + // const platformData = this.$platformsDataService.getPlatformData(settings.platform, projectData); + + // Of the properties below only `buildForDevice` and `release` are currently used. + // Leaving the others with placeholder values so that they may not be forgotten in future implementations. + const debugInfo = await this.$debugService.debug(debugData, settings.debugOptions); + const result = this.printDebugInformation(debugInfo, settings.debugOptions.forceDebuggerAttachedEvent); + return result; + } + + public printDebugInformation(debugInformation: IDebugInformation, fireDebuggerAttachedEvent: boolean = true): IDebugInformation { + if (!!debugInformation.url) { + if (fireDebuggerAttachedEvent) { + this.$runEmitter.emitDebuggerAttachedEvent(debugInformation); + } + + this.$logger.info(`To start debugging, open the following URL in Chrome:${EOL}${debugInformation.url}${EOL}`.cyan); + } + + return debugInformation; + } + + @performanceLog() + private async enableDebuggingCoreWithoutWaitingCurrentAction(deviceOption: IEnableDebuggingDeviceOptions, deviceDescriptor: ILiveSyncDeviceInfo, debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise { + if (!deviceDescriptor) { + this.$errors.failWithoutHelp(`Couldn't enable debugging for ${deviceOption.deviceIdentifier}`); + } + + deviceDescriptor.debuggingEnabled = true; + deviceDescriptor.debugOptions = deviceOption.debugOptions; + const currentDeviceInstance = this.$devicesService.getDeviceByIdentifier(deviceOption.deviceIdentifier); + const attachDebuggerOptions: IAttachDebuggerOptions = { + deviceIdentifier: deviceOption.deviceIdentifier, + isEmulator: currentDeviceInstance.isEmulator, + outputPath: deviceDescriptor.outputPath, + platform: currentDeviceInstance.deviceInfo.platform, + projectDir: debuggingAdditionalOptions.projectDir, + debugOptions: deviceOption.debugOptions + }; + + let debugInformation: IDebugInformation; + try { + debugInformation = await this.attachDebugger(attachDebuggerOptions); + } catch (err) { + this.$logger.trace("Couldn't attach debugger, will modify options and try again.", err); + attachDebuggerOptions.debugOptions.start = false; + try { + debugInformation = await this.attachDebugger(attachDebuggerOptions); + } catch (innerErr) { + this.$logger.trace("Couldn't attach debugger with modified options.", innerErr); + throw err; + } + } + + return debugInformation; + } +} +$injector.register("deviceDebugAppService", DeviceDebugAppService); diff --git a/lib/services/device/device-install-app-service.ts b/lib/services/device/device-install-app-service.ts new file mode 100644 index 0000000000..3cd6853e9b --- /dev/null +++ b/lib/services/device/device-install-app-service.ts @@ -0,0 +1,119 @@ +import { TrackActionNames, HASHES_FILE_NAME } from "../../constants"; +import * as helpers from "../../common/helpers"; +import * as path from "path"; + +const buildInfoFileName = ".nsbuildinfo"; + +export class DeviceInstallAppService { + constructor( + private $analyticsService: IAnalyticsService, + private $buildArtefactsService: IBuildArtefactsService, + private $devicePathProvider: IDevicePathProvider, + private $fs: IFileSystem, + private $logger: ILogger, + private $mobileHelper: Mobile.IMobileHelper, + private $buildInfoFileService: IBuildInfoFileService, + private $projectDataService: IProjectDataService, + private $platformsDataService: IPlatformsDataService + ) { } + + public async installOnDevice(device: Mobile.IDevice, buildData: IBuildData, packageFile?: string): Promise { + this.$logger.out(`Installing on device ${device.deviceInfo.identifier}...`); + + const projectData = this.$projectDataService.getProjectData(buildData.projectDir); + const platformData = this.$platformsDataService.getPlatformData(device.deviceInfo.platform, projectData); + + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: TrackActionNames.Deploy, + device, + projectDir: projectData.projectDir + }); + + if (!packageFile) { + packageFile = await this.$buildArtefactsService.getLatestApplicationPackagePath(platformData, buildData); + } + + await platformData.platformProjectService.cleanDeviceTempFolder(device.deviceInfo.identifier, projectData); + + const platform = device.deviceInfo.platform.toLowerCase(); + await device.applicationManager.reinstallApplication(projectData.projectIdentifiers[platform], packageFile); + + const outputFilePath = buildData.outputPath; + + await this.updateHashesOnDevice({ + device, + appIdentifier: projectData.projectIdentifiers[platform], + outputFilePath, + platformData + }); + + if (!buildData.release) { + const deviceFilePath = await this.getDeviceBuildInfoFilePath(device, projectData); + const options = buildData; + options.buildForDevice = !device.isEmulator; + const buildInfoFilePath = outputFilePath || platformData.getBuildOutputPath(buildData); + const appIdentifier = projectData.projectIdentifiers[platform]; + + await device.fileSystem.putFile(path.join(buildInfoFilePath, buildInfoFileName), deviceFilePath, appIdentifier); + } + + this.$logger.out(`Successfully installed on device with identifier '${device.deviceInfo.identifier}'.`); + } + + public async installOnDeviceIfNeeded(device: Mobile.IDevice, buildData: IBuildData, packageFile?: string): Promise { + const shouldInstall = await this.shouldInstall(device, buildData); + if (shouldInstall) { + await this.installOnDevice(device, buildData, packageFile); + } + } + + public async getDeviceBuildInfoFilePath(device: Mobile.IDevice, projectData: IProjectData): Promise { + const platform = device.deviceInfo.platform.toLowerCase(); + const deviceRootPath = await this.$devicePathProvider.getDeviceProjectRootPath(device, { + appIdentifier: projectData.projectIdentifiers[platform], + getDirname: true + }); + return helpers.fromWindowsRelativePathToUnix(path.join(deviceRootPath, buildInfoFileName)); + } + + public async shouldInstall(device: Mobile.IDevice, buildData: IBuildData): Promise { + const projectData = this.$projectDataService.getProjectData(buildData.projectDir); + const platformData = this.$platformsDataService.getPlatformData(device.deviceInfo.platform, projectData); + const platform = device.deviceInfo.platform; + if (!(await device.applicationManager.isApplicationInstalled(projectData.projectIdentifiers[platform.toLowerCase()]))) { + return true; + } + + const deviceBuildInfo: IBuildInfo = await this.getDeviceBuildInfo(device, projectData); + const localBuildInfo = this.$buildInfoFileService.getBuildInfoFromFile(platformData, { ...buildData, buildForDevice: !device.isEmulator }); + + return !localBuildInfo || !deviceBuildInfo || deviceBuildInfo.buildTime !== localBuildInfo.buildTime; + } + + private async updateHashesOnDevice(data: { device: Mobile.IDevice, appIdentifier: string, outputFilePath: string, platformData: IPlatformData }): Promise { + const { device, appIdentifier, platformData, outputFilePath } = data; + + if (!this.$mobileHelper.isAndroidPlatform(platformData.normalizedPlatformName)) { + return; + } + + let hashes = {}; + const hashesFilePath = path.join(outputFilePath || platformData.getBuildOutputPath(null), HASHES_FILE_NAME); + if (this.$fs.exists(hashesFilePath)) { + hashes = this.$fs.readJson(hashesFilePath); + } + + await device.fileSystem.updateHashesOnDevice(hashes, appIdentifier); + } + + private async getDeviceBuildInfo(device: Mobile.IDevice, projectData: IProjectData): Promise { + const deviceFilePath = await this.getDeviceBuildInfoFilePath(device, projectData); + try { + const deviceFileContent = await this.$mobileHelper.getDeviceFileContent(device, deviceFilePath, projectData); + return JSON.parse(deviceFileContent); + } catch (e) { + return null; + } + } +} +$injector.register("deviceInstallAppService", DeviceInstallAppService); diff --git a/lib/services/device/device-refresh-app-service.ts b/lib/services/device/device-refresh-app-service.ts new file mode 100644 index 0000000000..db2656f3a9 --- /dev/null +++ b/lib/services/device/device-refresh-app-service.ts @@ -0,0 +1,51 @@ +import { performanceLog } from "../../common/decorators"; +import { RunEmitter } from "../../emitters/run-emitter"; +import { LiveSyncServiceResolver } from "../../resolvers/livesync-service-resolver"; + +export class DeviceRefreshAppService implements IDeviceRefreshAppService { + + constructor( + private $liveSyncServiceResolver: LiveSyncServiceResolver, + private $logger: ILogger, + private $runEmitter: RunEmitter + ) { } + + @performanceLog() + public async refreshApplication(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, deviceDescriptor: ILiveSyncDeviceInfo, settings?: IRefreshApplicationSettings): Promise { + if (deviceDescriptor && deviceDescriptor.debuggingEnabled) { + liveSyncResultInfo.waitForDebugger = deviceDescriptor.debugOptions && deviceDescriptor.debugOptions.debugBrk; + } + + const result = { didRestart: false }; + const platform = liveSyncResultInfo.deviceAppData.platform; + const applicationIdentifier = projectData.projectIdentifiers[platform.toLowerCase()]; + const platformLiveSyncService = this.$liveSyncServiceResolver.resolveLiveSyncService(platform); + + try { + let shouldRestart = await platformLiveSyncService.shouldRestart(projectData, liveSyncResultInfo); + if (!shouldRestart) { + shouldRestart = !await platformLiveSyncService.tryRefreshApplication(projectData, liveSyncResultInfo); + } + + if (shouldRestart) { + this.$runEmitter.emitDebuggerDetachedEvent(liveSyncResultInfo.deviceAppData.device); + await platformLiveSyncService.restartApplication(projectData, liveSyncResultInfo); + result.didRestart = true; + } + } catch (err) { + this.$logger.info(`Error while trying to start application ${applicationIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}. Error is: ${err.message || err}`); + const msg = `Unable to start application ${applicationIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}. Try starting it manually.`; + this.$logger.warn(msg); + if (!settings || !settings.shouldSkipEmitLiveSyncNotification) { + this.$runEmitter.emitRunNotificationEvent(projectData, liveSyncResultInfo.deviceAppData.device, msg); + } + + if (settings && settings.shouldCheckDeveloperDiscImage && (err.message || err) === "Could not find developer disk image") { + this.$runEmitter.emitUserInteractionNeededEvent(projectData, liveSyncResultInfo.deviceAppData.device, deviceDescriptor); + } + } + + return result; + } +} +$injector.register("deviceRefreshAppService", DeviceRefreshAppService); diff --git a/lib/services/emulator-settings-service.ts b/lib/services/emulator-settings-service.ts deleted file mode 100644 index d9bd1b6f26..0000000000 --- a/lib/services/emulator-settings-service.ts +++ /dev/null @@ -1,17 +0,0 @@ -export class EmulatorSettingsService implements Mobile.IEmulatorSettingsService { - private static REQURED_ANDROID_APILEVEL = 17; - - constructor(private $injector: IInjector) { } - - public canStart(platform: string): boolean { - const platformService = this.$injector.resolve("platformService"); // this should be resolved here due to cyclic dependency - - const installedPlatforms = platformService.getInstalledPlatforms(); - return _.includes(installedPlatforms, platform.toLowerCase()); - } - - public get minVersion(): number { - return EmulatorSettingsService.REQURED_ANDROID_APILEVEL; - } -} -$injector.register("emulatorSettingsService", EmulatorSettingsService); diff --git a/lib/services/init-service.ts b/lib/services/init-service.ts index 8a35b0af0f..b902b8df17 100644 --- a/lib/services/init-service.ts +++ b/lib/services/init-service.ts @@ -35,7 +35,7 @@ export class InitService implements IInitService { if (!projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE]) { projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE] = {}; - this.$fs.writeJson(this.projectFilePath, projectData); // We need to create package.json file here in order to prevent "No project found at or above and neither was a --path specified." when resolving platformsData + this.$fs.writeJson(this.projectFilePath, projectData); // We need to create package.json file here in order to prevent "No project found at or above and neither was a --path specified." when resolving platformsDataService } try { @@ -46,11 +46,12 @@ export class InitService implements IInitService { projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][this.$options.frameworkName] = _.extend(currentPlatformData, this.buildVersionData(this.$options.frameworkVersion)); } else { - const $platformsData = this.$injector.resolve("platformsData"); + const $mobileHelper: Mobile.IMobileHelper = this.$injector.resolve("mobileHelper"); + const $platformsDataService = this.$injector.resolve("platformsDataService"); const $projectData = this.$injector.resolve("projectData"); $projectData.initializeProjectData(path.dirname(this.projectFilePath)); - for (const platform of $platformsData.platformsNames) { - const platformData: IPlatformData = $platformsData.getPlatformData(platform, $projectData); + for (const platform of $mobileHelper.platformNames) { + const platformData: IPlatformData = $platformsDataService.getPlatformData(platform, $projectData); if (!platformData.targetedOS || (platformData.targetedOS && _.includes(platformData.targetedOS, process.platform))) { const currentPlatformData = projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][platformData.frameworkPackageName] || {}; diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 33fd672742..32afea3cdb 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -12,6 +12,8 @@ import * as plist from "plist"; import { IOSProvisionService } from "./ios-provision-service"; import { IOSEntitlementsService } from "./ios-entitlements-service"; import { BUILD_XCCONFIG_FILE_NAME, IosProjectConstants } from "../constants"; +import { IOSBuildData } from "../data/build-data"; +import { IOSPrepareData } from "../data/prepare-data"; interface INativeSourceCodeGroup { name: string; @@ -25,7 +27,7 @@ const SimulatorPlatformSdkName = "iphonesimulator"; const getPlatformSdkName = (forDevice: boolean): string => forDevice ? DevicePlatformSdkName : SimulatorPlatformSdkName; const getConfigurationName = (release: boolean): string => release ? Configurations.Release : Configurations.Debug; -export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase implements IPlatformProjectService { +export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase { private static IOS_PROJECT_NAME_PLACEHOLDER = "__PROJECT_NAME__"; private static IOS_PLATFORM_NAME = "ios"; @@ -65,8 +67,9 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ this._platformData = { frameworkPackageName: constants.TNS_IOS_RUNTIME_NAME, normalizedPlatformName: "iOS", + platformNameLowerCase: "ios", appDestinationDirectoryPath: path.join(projectRoot, projectData.projectName), - platformProjectService: this, + platformProjectService: this, projectRoot: projectRoot, getBuildOutputPath: (options: IBuildOutputOptions): string => { const config = getConfigurationName(!options || options.release); @@ -137,23 +140,13 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ }; } - // TODO: Remove Promise, reason: readDirectory - unable until androidProjectService has async operations. - public async createProject(frameworkDir: string, frameworkVersion: string, projectData: IProjectData, config: ICreateProjectOptions): Promise { + public async createProject(frameworkDir: string, frameworkVersion: string, projectData: IProjectData): Promise { this.$fs.ensureDirectoryExists(path.join(this.getPlatformData(projectData).projectRoot, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER)); - if (config.pathToTemplate) { - // Copy everything except the template from the runtime - this.$fs.readDirectory(frameworkDir) - .filter(dirName => dirName.indexOf(IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER) === -1) - .forEach(dirName => shell.cp("-R", path.join(frameworkDir, dirName), this.getPlatformData(projectData).projectRoot)); - shell.cp("-rf", path.join(config.pathToTemplate, "*"), this.getPlatformData(projectData).projectRoot); - } else { - shell.cp("-R", path.join(frameworkDir, "*"), this.getPlatformData(projectData).projectRoot); - } - + shell.cp("-R", path.join(frameworkDir, "*"), this.getPlatformData(projectData).projectRoot); } //TODO: plamen5kov: revisit this method, might have unnecessary/obsolete logic - public async interpolateData(projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise { + public async interpolateData(projectData: IProjectData): Promise { const projectRootFilePath = path.join(this.getPlatformData(projectData).projectRoot, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER); // Starting with NativeScript for iOS 1.6.0, the project Info.plist file resides not in the platform project, // but in the hello-world app template as a platform specific resource. @@ -182,32 +175,41 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ this.replaceFileContent(pbxprojFilePath, projectData); } - public interpolateConfigurationFile(projectData: IProjectData, platformSpecificData: IPlatformSpecificData): void { + public interpolateConfigurationFile(projectData: IProjectData): void { return undefined; } + public async cleanProject(projectRoot: string, projectData: IProjectData): Promise { + return null; + } + public afterCreateProject(projectRoot: string, projectData: IProjectData): void { this.$fs.rename(path.join(projectRoot, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER), path.join(projectRoot, projectData.projectName)); } - public async buildProject(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + public async buildProject(projectRoot: string, projectData: IProjectData, iOSBuildData: IOSBuildData): Promise { const platformData = this.getPlatformData(projectData); const handler = (data: any) => { this.emit(constants.BUILD_OUTPUT_EVENT_NAME, data); }; - if (buildConfig.buildForDevice) { - await this.$iOSSigningService.setupSigningForDevice(projectRoot, projectData, buildConfig); + if (iOSBuildData.buildForDevice) { + await this.$iOSSigningService.setupSigningForDevice(projectRoot, projectData, iOSBuildData); + await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME, + this.$childProcess, + handler, + this.$xcodebuildService.buildForDevice(platformData, projectData, iOSBuildData)); + } else if (iOSBuildData.buildForAppStore) { await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME, this.$childProcess, handler, - this.$xcodebuildService.buildForDevice(platformData, projectData, buildConfig)); + this.$xcodebuildService.buildForAppStore(platformData, projectData, iOSBuildData)); } else { await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME, this.$childProcess, handler, - this.$xcodebuildService.buildForSimulator(platformData, projectData, buildConfig)); + this.$xcodebuildService.buildForSimulator(platformData, projectData, iOSBuildData)); } this.validateApplicationIdentifier(projectData); @@ -275,13 +277,13 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ return contentIsTheSame; } - public async prepareProject(projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise { + public async prepareProject(projectData: IProjectData, prepareData: IOSPrepareData): Promise { const projectRoot = path.join(projectData.platformsDir, "ios"); - const provision = platformSpecificData && platformSpecificData.provision; - const teamId = platformSpecificData && platformSpecificData.teamId; + const provision = prepareData && prepareData.provision; + const teamId = prepareData && prepareData.teamId; if (provision) { - await this.$iOSSigningService.setupSigningFromProvision(projectRoot, projectData, provision, platformSpecificData.mobileProvisionData); + await this.$iOSSigningService.setupSigningFromProvision(projectRoot, projectData, provision, prepareData.mobileProvisionData); } if (teamId) { await this.$iOSSigningService.setupSigningFromTeam(projectRoot, projectData, teamId); @@ -506,7 +508,8 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ return Promise.resolve(); } - public async checkForChanges(changesInfo: IProjectChangesInfo, { provision, teamId }: IProjectChangesOptions, projectData: IProjectData): Promise { + public async checkForChanges(changesInfo: IProjectChangesInfo, prepareData: IOSPrepareData, projectData: IProjectData): Promise { + const { provision, teamId } = prepareData; const hasProvision = provision !== undefined; const hasTeamId = teamId !== undefined; if (hasProvision || hasTeamId) { diff --git a/lib/services/ios/ios-signing-service.ts b/lib/services/ios/ios-signing-service.ts index ae92d8a28d..3669f53a6d 100644 --- a/lib/services/ios/ios-signing-service.ts +++ b/lib/services/ios/ios-signing-service.ts @@ -3,6 +3,7 @@ import * as mobileProvisionFinder from "ios-mobileprovision-finder"; import { BUILD_XCCONFIG_FILE_NAME, iOSAppResourcesFolderName } from "../../constants"; import * as helpers from "../../common/helpers"; import { IOSProvisionService } from "../ios-provision-service"; +import { IOSBuildData } from "../../data/build-data"; export class IOSSigningService implements IiOSSigningService { constructor( @@ -16,7 +17,7 @@ export class IOSSigningService implements IiOSSigningService { private $xcprojService: IXcprojService ) { } - public async setupSigningForDevice(projectRoot: string, projectData: IProjectData, buildConfig: IiOSBuildConfig): Promise { + public async setupSigningForDevice(projectRoot: string, projectData: IProjectData, iOSBuildData: IOSBuildData): Promise { const xcode = this.$pbxprojDomXcode.Xcode.open(this.getPbxProjPath(projectData, projectRoot)); const signing = xcode.getSigning(projectData.projectName); @@ -29,8 +30,8 @@ export class IOSSigningService implements IiOSSigningService { if (hasProvisioningProfileInXCConfig && (!signing || signing.style !== "Manual")) { xcode.setManualSigningStyle(projectData.projectName); xcode.save(); - } else if (!buildConfig.provision && !(signing && signing.style === "Manual" && !buildConfig.teamId)) { - const teamId = await this.getDevelopmentTeam(projectData, projectRoot, buildConfig.teamId); + } else if (!iOSBuildData.provision && !(signing && signing.style === "Manual" && !iOSBuildData.teamId)) { + const teamId = await this.getDevelopmentTeam(projectData, projectRoot, iOSBuildData.teamId); await this.setupSigningFromTeam(projectRoot, projectData, teamId); } } diff --git a/lib/services/livesync/android-device-livesync-service-base.ts b/lib/services/livesync/android-device-livesync-service-base.ts index d8aa61f351..2459d625dd 100644 --- a/lib/services/livesync/android-device-livesync-service-base.ts +++ b/lib/services/livesync/android-device-livesync-service-base.ts @@ -2,11 +2,11 @@ import { DeviceLiveSyncServiceBase } from './device-livesync-service-base'; export abstract class AndroidDeviceLiveSyncServiceBase extends DeviceLiveSyncServiceBase { constructor(protected $injector: IInjector, - protected $platformsData: IPlatformsData, + protected $platformsDataService: IPlatformsDataService, protected $filesHashService: IFilesHashService, protected $logger: ILogger, protected device: Mobile.IAndroidDevice) { - super($platformsData, device); + super($platformsDataService, device); } public abstract async transferFilesOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise; diff --git a/lib/services/livesync/android-device-livesync-service.ts b/lib/services/livesync/android-device-livesync-service.ts index b67fcc3614..b4cc66b453 100644 --- a/lib/services/livesync/android-device-livesync-service.ts +++ b/lib/services/livesync/android-device-livesync-service.ts @@ -12,11 +12,11 @@ export class AndroidDeviceLiveSyncService extends AndroidDeviceLiveSyncServiceBa private $devicePathProvider: IDevicePathProvider, $injector: IInjector, private $androidProcessService: Mobile.IAndroidProcessService, - protected $platformsData: IPlatformsData, + protected platformsDataService: IPlatformsDataService, protected device: Mobile.IAndroidDevice, $filesHashService: IFilesHashService, $logger: ILogger) { - super($injector, $platformsData, $filesHashService, $logger, device); + super($injector, platformsDataService, $filesHashService, $logger, device); } public async transferFilesOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { diff --git a/lib/services/livesync/android-device-livesync-sockets-service.ts b/lib/services/livesync/android-device-livesync-sockets-service.ts index f9efc9d11a..06f9bd5f40 100644 --- a/lib/services/livesync/android-device-livesync-sockets-service.ts +++ b/lib/services/livesync/android-device-livesync-sockets-service.ts @@ -14,7 +14,7 @@ export class AndroidDeviceSocketsLiveSyncService extends AndroidDeviceLiveSyncSe constructor( private data: IProjectData, $injector: IInjector, - protected $platformsData: IPlatformsData, + protected platformsDataService: IPlatformsDataService, protected $staticConfig: Config.IStaticConfig, $logger: ILogger, protected device: Mobile.IAndroidDevice, @@ -23,7 +23,7 @@ export class AndroidDeviceSocketsLiveSyncService extends AndroidDeviceLiveSyncSe private $fs: IFileSystem, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, $filesHashService: IFilesHashService) { - super($injector, $platformsData, $filesHashService, $logger, device); + super($injector, platformsDataService, $filesHashService, $logger, device); this.livesyncTool = this.$injector.resolve(AndroidLivesyncTool); } @@ -147,7 +147,7 @@ export class AndroidDeviceSocketsLiveSyncService extends AndroidDeviceLiveSyncSe } private async connectLivesyncTool(appIdentifier: string, connectTimeout?: number) { - const platformData = this.$platformsData.getPlatformData(this.$devicePlatformsConstants.Android, this.data); + const platformData = this.platformsDataService.getPlatformData(this.$devicePlatformsConstants.Android, this.data); const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); if (!this.livesyncTool.hasConnection()) { await this.livesyncTool.connect({ diff --git a/lib/services/livesync/android-livesync-service.ts b/lib/services/livesync/android-livesync-service.ts index 70f765ae3e..d3abc27b2e 100644 --- a/lib/services/livesync/android-livesync-service.ts +++ b/lib/services/livesync/android-livesync-service.ts @@ -6,14 +6,13 @@ import * as semver from "semver"; export class AndroidLiveSyncService extends PlatformLiveSyncServiceBase implements IPlatformLiveSyncService { private static MIN_SOCKETS_LIVESYNC_RUNTIME_VERSION = "4.2.0-2018-07-20-02"; - constructor(protected $platformsData: IPlatformsData, + constructor(protected $platformsDataService: IPlatformsDataService, protected $projectFilesManager: IProjectFilesManager, private $injector: IInjector, $devicePathProvider: IDevicePathProvider, $fs: IFileSystem, - $logger: ILogger, - $projectFilesProvider: IProjectFilesProvider) { - super($fs, $logger, $platformsData, $projectFilesManager, $devicePathProvider, $projectFilesProvider); + $logger: ILogger) { + super($fs, $logger, $platformsDataService, $projectFilesManager, $devicePathProvider); } protected _getDeviceLiveSyncService(device: Mobile.IDevice, data: IProjectDir, frameworkVersion: string): INativeScriptDeviceLiveSyncService { diff --git a/lib/services/livesync/device-livesync-service-base.ts b/lib/services/livesync/device-livesync-service-base.ts index 62f9a8aad5..c10c23fd7d 100644 --- a/lib/services/livesync/device-livesync-service-base.ts +++ b/lib/services/livesync/device-livesync-service-base.ts @@ -6,7 +6,7 @@ export abstract class DeviceLiveSyncServiceBase { private static FAST_SYNC_FILE_EXTENSIONS = [".css", ".xml", ".html"]; constructor( - protected $platformsData: IPlatformsData, + protected platformsDataService: IPlatformsDataService, protected device: Mobile.IDevice ) { } @@ -23,7 +23,7 @@ export abstract class DeviceLiveSyncServiceBase { @cache() private getFastLiveSyncFileExtensions(platform: string, projectData: IProjectData): string[] { - const platformData = this.$platformsData.getPlatformData(platform, projectData); + const platformData = this.platformsDataService.getPlatformData(platform, projectData); const fastSyncFileExtensions = DeviceLiveSyncServiceBase.FAST_SYNC_FILE_EXTENSIONS.concat(platformData.fastLivesyncFileExtensions); return fastSyncFileExtensions; } diff --git a/lib/services/livesync/ios-device-livesync-service.ts b/lib/services/livesync/ios-device-livesync-service.ts index af027caffd..497bb66988 100644 --- a/lib/services/livesync/ios-device-livesync-service.ts +++ b/lib/services/livesync/ios-device-livesync-service.ts @@ -11,9 +11,9 @@ export class IOSDeviceLiveSyncService extends DeviceLiveSyncServiceBase implemen constructor( private $logger: ILogger, - protected $platformsData: IPlatformsData, + protected platformsDataService: IPlatformsDataService, protected device: Mobile.IiOSDevice) { - super($platformsData, device); + super(platformsDataService, device); } private async setupSocketIfNeeded(projectData: IProjectData): Promise { diff --git a/lib/services/livesync/ios-livesync-service.ts b/lib/services/livesync/ios-livesync-service.ts index 16566740b2..89b367616e 100644 --- a/lib/services/livesync/ios-livesync-service.ts +++ b/lib/services/livesync/ios-livesync-service.ts @@ -8,13 +8,12 @@ import { performanceLog } from "../../common/decorators"; export class IOSLiveSyncService extends PlatformLiveSyncServiceBase implements IPlatformLiveSyncService { constructor(protected $fs: IFileSystem, - protected $platformsData: IPlatformsData, + protected $platformsDataService: IPlatformsDataService, protected $projectFilesManager: IProjectFilesManager, private $injector: IInjector, $devicePathProvider: IDevicePathProvider, - $logger: ILogger, - $projectFilesProvider: IProjectFilesProvider) { - super($fs, $logger, $platformsData, $projectFilesManager, $devicePathProvider, $projectFilesProvider); + $logger: ILogger) { + super($fs, $logger, $platformsDataService, $projectFilesManager, $devicePathProvider); } @performanceLog() @@ -25,7 +24,7 @@ export class IOSLiveSyncService extends PlatformLiveSyncServiceBase implements I return super.fullSync(syncInfo); } const projectData = syncInfo.projectData; - const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); + const platformData = this.$platformsDataService.getPlatformData(device.deviceInfo.platform, projectData); const deviceAppData = await this.getAppData(syncInfo); const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); @@ -35,9 +34,7 @@ export class IOSLiveSyncService extends PlatformLiveSyncServiceBase implements I this.$logger.trace("Creating zip file: " + tempZip); this.$fs.copyFile(path.join(path.dirname(projectFilesPath), `${APP_FOLDER_NAME}/*`), tempApp); - if (!syncInfo.syncAllFiles) { - this.$fs.deleteDirectory(path.join(tempApp, TNS_MODULES_FOLDER_NAME)); - } + this.$fs.deleteDirectory(path.join(tempApp, TNS_MODULES_FOLDER_NAME)); await this.$fs.zipFiles(tempZip, this.$fs.enumerateFilesInDirectorySync(tempApp), (res) => { return path.join(APP_FOLDER_NAME, path.relative(tempApp, res)); @@ -64,7 +61,7 @@ export class IOSLiveSyncService extends PlatformLiveSyncServiceBase implements I // In this case we should execute fullsync because iOS Runtime requires the full content of app dir to be extracted in the root of sync dir. return this.fullSync({ projectData: liveSyncInfo.projectData, - device, syncAllFiles: liveSyncInfo.syncAllFiles, + device, liveSyncDeviceInfo: liveSyncInfo.liveSyncDeviceInfo, watch: true, useHotModuleReload: liveSyncInfo.useHotModuleReload diff --git a/lib/services/livesync/livesync-service.ts b/lib/services/livesync/livesync-service.ts deleted file mode 100644 index 5d5e800bb6..0000000000 --- a/lib/services/livesync/livesync-service.ts +++ /dev/null @@ -1,881 +0,0 @@ -import * as path from "path"; -import * as choki from "chokidar"; -import { EOL } from "os"; -import { EventEmitter } from "events"; -import { hook } from "../../common/helpers"; -import { - PACKAGE_JSON_FILE_NAME, - USER_INTERACTION_NEEDED_EVENT_NAME, - DEBUGGER_ATTACHED_EVENT_NAME, - DEBUGGER_DETACHED_EVENT_NAME, - TrackActionNames, - LiveSyncEvents -} from "../../constants"; -import { DeviceTypes, DeviceDiscoveryEventNames, HmrConstants } from "../../common/constants"; -import { cache } from "../../common/decorators"; -import { performanceLog } from "../../common/decorators"; - -const deviceDescriptorPrimaryKey = "identifier"; - -export class LiveSyncService extends EventEmitter implements IDebugLiveSyncService { - // key is projectDir - protected liveSyncProcessesInfo: IDictionary = {}; - - constructor(private $platformService: IPlatformService, - private $projectDataService: IProjectDataService, - private $devicesService: Mobile.IDevicesService, - private $mobileHelper: Mobile.IMobileHelper, - private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder, - private $logger: ILogger, - private $hooksService: IHooksService, - private $pluginsService: IPluginsService, - private $debugService: IDebugService, - private $errors: IErrors, - private $debugDataService: IDebugDataService, - private $analyticsService: IAnalyticsService, - private $usbLiveSyncService: DeprecatedUsbLiveSyncService, - private $previewAppLiveSyncService: IPreviewAppLiveSyncService, - private $previewQrCodeService: IPreviewQrCodeService, - private $previewSdkService: IPreviewSdkService, - private $hmrStatusService: IHmrStatusService, - private $injector: IInjector) { - super(); - } - - public async liveSync(deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise { - const projectData = this.$projectDataService.getProjectData(liveSyncData.projectDir); - await this.$pluginsService.ensureAllDependenciesAreInstalled(projectData); - await this.liveSyncOperation(deviceDescriptors, liveSyncData, projectData); - } - - public async liveSyncToPreviewApp(data: IPreviewAppLiveSyncData): Promise { - this.attachToPreviewAppLiveSyncError(); - - await this.liveSync([], { - syncToPreviewApp: true, - projectDir: data.projectDir, - bundle: data.bundle, - useHotModuleReload: data.useHotModuleReload, - release: false, - env: data.env, - }); - - const url = this.$previewSdkService.getQrCodeUrl({ projectDir: data.projectDir, useHotModuleReload: data.useHotModuleReload }); - const result = await this.$previewQrCodeService.getLiveSyncQrCode(url); - return result; - } - - public async stopLiveSync(projectDir: string, deviceIdentifiers?: string[], stopOptions?: { shouldAwaitAllActions: boolean }): Promise { - const liveSyncProcessInfo = this.liveSyncProcessesInfo[projectDir]; - if (liveSyncProcessInfo && !liveSyncProcessInfo.isStopped) { - // In case we are coming from error during livesync, the current action is the one that erred (but we are still executing it), - // so we cannot await it as this will cause infinite loop. - const shouldAwaitPendingOperation = !stopOptions || stopOptions.shouldAwaitAllActions; - - const deviceIdentifiersToRemove = deviceIdentifiers || _.map(liveSyncProcessInfo.deviceDescriptors, d => d.identifier); - - const removedDeviceIdentifiers = _.remove(liveSyncProcessInfo.deviceDescriptors, descriptor => _.includes(deviceIdentifiersToRemove, descriptor.identifier)) - .map(descriptor => descriptor.identifier); - - // In case deviceIdentifiers are not passed, we should stop the whole LiveSync. - if (!deviceIdentifiers || !deviceIdentifiers.length || !liveSyncProcessInfo.deviceDescriptors || !liveSyncProcessInfo.deviceDescriptors.length) { - if (liveSyncProcessInfo.timer) { - clearTimeout(liveSyncProcessInfo.timer); - } - - if (liveSyncProcessInfo.watcherInfo && liveSyncProcessInfo.watcherInfo.watcher) { - liveSyncProcessInfo.watcherInfo.watcher.close(); - } - - liveSyncProcessInfo.watcherInfo = null; - liveSyncProcessInfo.isStopped = true; - - if (liveSyncProcessInfo.actionsChain && shouldAwaitPendingOperation) { - await liveSyncProcessInfo.actionsChain; - } - - liveSyncProcessInfo.deviceDescriptors = []; - - if (liveSyncProcessInfo.syncToPreviewApp) { - await this.$previewAppLiveSyncService.stopLiveSync(); - this.$previewAppLiveSyncService.removeAllListeners(); - } - - // Kill typescript watcher - const projectData = this.$projectDataService.getProjectData(projectDir); - await this.$hooksService.executeAfterHooks('watch', { - hookArgs: { - projectData - } - }); - - // In case we are stopping the LiveSync we must set usbLiveSyncService.isInitialized to false, - // as in case we execute nativescript-dev-typescript's before-prepare hook again in the same process, it MUST transpile the files. - this.$usbLiveSyncService.isInitialized = false; - } else if (liveSyncProcessInfo.currentSyncAction && shouldAwaitPendingOperation) { - await liveSyncProcessInfo.currentSyncAction; - } - - // Emit LiveSync stopped when we've really stopped. - _.each(removedDeviceIdentifiers, deviceIdentifier => { - this.emitLivesyncEvent(LiveSyncEvents.liveSyncStopped, { projectDir, deviceIdentifier }); - }); - } - } - - public getLiveSyncDeviceDescriptors(projectDir: string): ILiveSyncDeviceInfo[] { - const liveSyncProcessesInfo = this.liveSyncProcessesInfo[projectDir] || {}; - const currentDescriptors = liveSyncProcessesInfo.deviceDescriptors; - return currentDescriptors || []; - } - - private attachToPreviewAppLiveSyncError(): void { - if (!this.$usbLiveSyncService.isInitialized) { - this.$previewAppLiveSyncService.on(LiveSyncEvents.previewAppLiveSyncError, liveSyncData => { - this.$logger.error(liveSyncData.error); - this.emit(LiveSyncEvents.previewAppLiveSyncError, liveSyncData); - }); - } - } - - @performanceLog() - private async refreshApplication(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, debugOpts?: IDebugOptions, outputPath?: string): Promise { - const deviceDescriptor = this.getDeviceDescriptor(liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier, projectData.projectDir); - - return deviceDescriptor && deviceDescriptor.debugggingEnabled ? - this.refreshApplicationWithDebug(projectData, liveSyncResultInfo, debugOpts, outputPath) : - this.refreshApplicationWithoutDebug(projectData, liveSyncResultInfo, debugOpts, outputPath); - } - - private async refreshApplicationWithoutDebug(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, debugOpts?: IDebugOptions, outputPath?: string, settings?: IRefreshApplicationSettings): Promise { - const result = { didRestart: false }; - const platform = liveSyncResultInfo.deviceAppData.platform; - const platformLiveSyncService = this.getLiveSyncService(platform); - const applicationIdentifier = projectData.projectIdentifiers[platform.toLowerCase()]; - try { - let shouldRestart = await platformLiveSyncService.shouldRestart(projectData, liveSyncResultInfo); - if (!shouldRestart) { - shouldRestart = !await platformLiveSyncService.tryRefreshApplication(projectData, liveSyncResultInfo); - } - - if (shouldRestart) { - const deviceIdentifier = liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier; - this.emit(DEBUGGER_DETACHED_EVENT_NAME, { deviceIdentifier }); - await platformLiveSyncService.restartApplication(projectData, liveSyncResultInfo); - result.didRestart = true; - } - } catch (err) { - this.$logger.info(`Error while trying to start application ${applicationIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}. Error is: ${err.message || err}`); - const msg = `Unable to start application ${applicationIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}. Try starting it manually.`; - this.$logger.warn(msg); - if (!settings || !settings.shouldSkipEmitLiveSyncNotification) { - this.emitLivesyncEvent(LiveSyncEvents.liveSyncNotification, { - projectDir: projectData.projectDir, - applicationIdentifier, - deviceIdentifier: liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier, - notification: msg - }); - } - - if (settings && settings.shouldCheckDeveloperDiscImage) { - this.handleDeveloperDiskImageError(err, liveSyncResultInfo, projectData, debugOpts, outputPath); - } - } - - this.emitLivesyncEvent(LiveSyncEvents.liveSyncExecuted, { - projectDir: projectData.projectDir, - applicationIdentifier, - syncedFiles: liveSyncResultInfo.modifiedFilesData.map(m => m.getLocalPath()), - deviceIdentifier: liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier, - isFullSync: liveSyncResultInfo.isFullSync - }); - - return result; - } - - @performanceLog() - private async refreshApplicationWithDebug(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, debugOptions: IDebugOptions, outputPath?: string): Promise { - debugOptions = debugOptions || {}; - if (debugOptions.debugBrk) { - liveSyncResultInfo.waitForDebugger = true; - } - - const refreshInfo = await this.refreshApplicationWithoutDebug(projectData, liveSyncResultInfo, debugOptions, outputPath, { shouldSkipEmitLiveSyncNotification: true, shouldCheckDeveloperDiscImage: true }); - - // we do not stop the application when debugBrk is false, so we need to attach, instead of launch - // if we try to send the launch request, the debugger port will not be printed and the command will timeout - debugOptions.start = !debugOptions.debugBrk; - - debugOptions.forceDebuggerAttachedEvent = refreshInfo.didRestart; - const deviceOption = { - deviceIdentifier: liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier, - debugOptions: debugOptions, - }; - - return this.enableDebuggingCoreWithoutWaitingCurrentAction(deviceOption, { projectDir: projectData.projectDir }); - } - - private handleDeveloperDiskImageError(err: any, liveSyncResultInfo: ILiveSyncResultInfo, projectData: IProjectData, debugOpts: IDebugOptions, outputPath: string) { - if ((err.message || err) === "Could not find developer disk image") { - const deviceIdentifier = liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier; - const attachDebuggerOptions: IAttachDebuggerOptions = { - platform: liveSyncResultInfo.deviceAppData.device.deviceInfo.platform, - isEmulator: liveSyncResultInfo.deviceAppData.device.isEmulator, - projectDir: projectData.projectDir, - deviceIdentifier, - debugOptions: debugOpts, - outputPath - }; - this.emit(USER_INTERACTION_NEEDED_EVENT_NAME, attachDebuggerOptions); - } - } - - public async attachDebugger(settings: IAttachDebuggerOptions): Promise { - // Default values - if (settings.debugOptions) { - settings.debugOptions.chrome = settings.debugOptions.chrome === undefined ? true : settings.debugOptions.chrome; - settings.debugOptions.start = settings.debugOptions.start === undefined ? true : settings.debugOptions.start; - } else { - settings.debugOptions = { - chrome: true, - start: true - }; - } - - const projectData = this.$projectDataService.getProjectData(settings.projectDir); - const debugData = this.$debugDataService.createDebugData(projectData, { device: settings.deviceIdentifier }); - - // Of the properties below only `buildForDevice` and `release` are currently used. - // Leaving the others with placeholder values so that they may not be forgotten in future implementations. - const buildConfig = this.getInstallApplicationBuildConfig(settings.deviceIdentifier, settings.projectDir, { isEmulator: settings.isEmulator }); - debugData.pathToAppPackage = this.$platformService.lastOutputPath(settings.platform, buildConfig, projectData, settings.outputPath); - const debugInfo = await this.$debugService.debug(debugData, settings.debugOptions); - const result = this.printDebugInformation(debugInfo, settings.debugOptions.forceDebuggerAttachedEvent); - return result; - } - - public printDebugInformation(debugInformation: IDebugInformation, fireDebuggerAttachedEvent: boolean = true): IDebugInformation { - if (!!debugInformation.url) { - if (fireDebuggerAttachedEvent) { - this.emit(DEBUGGER_ATTACHED_EVENT_NAME, debugInformation); - } - - this.$logger.info(`To start debugging, open the following URL in Chrome:${EOL}${debugInformation.url}${EOL}`.cyan); - } - - return debugInformation; - } - - public enableDebugging(deviceOpts: IEnableDebuggingDeviceOptions[], debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise[] { - return _.map(deviceOpts, d => this.enableDebuggingCore(d, debuggingAdditionalOptions)); - } - - private getDeviceDescriptor(deviceIdentifier: string, projectDir: string) { - const deviceDescriptors = this.getLiveSyncDeviceDescriptors(projectDir); - - return _.find(deviceDescriptors, d => d.identifier === deviceIdentifier); - } - - @performanceLog() - private async enableDebuggingCoreWithoutWaitingCurrentAction(deviceOption: IEnableDebuggingDeviceOptions, debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise { - const currentDeviceDescriptor = this.getDeviceDescriptor(deviceOption.deviceIdentifier, debuggingAdditionalOptions.projectDir); - if (!currentDeviceDescriptor) { - this.$errors.failWithoutHelp(`Couldn't enable debugging for ${deviceOption.deviceIdentifier}`); - } - - currentDeviceDescriptor.debugggingEnabled = true; - currentDeviceDescriptor.debugOptions = deviceOption.debugOptions; - const currentDeviceInstance = this.$devicesService.getDeviceByIdentifier(deviceOption.deviceIdentifier); - const attachDebuggerOptions: IAttachDebuggerOptions = { - deviceIdentifier: deviceOption.deviceIdentifier, - isEmulator: currentDeviceInstance.isEmulator, - outputPath: currentDeviceDescriptor.outputPath, - platform: currentDeviceInstance.deviceInfo.platform, - projectDir: debuggingAdditionalOptions.projectDir, - debugOptions: deviceOption.debugOptions - }; - - let debugInformation: IDebugInformation; - try { - debugInformation = await this.attachDebugger(attachDebuggerOptions); - } catch (err) { - this.$logger.trace("Couldn't attach debugger, will modify options and try again.", err); - attachDebuggerOptions.debugOptions.start = false; - try { - debugInformation = await this.attachDebugger(attachDebuggerOptions); - } catch (innerErr) { - this.$logger.trace("Couldn't attach debugger with modified options.", innerErr); - throw err; - } - } - - return debugInformation; - } - - private async enableDebuggingCore(deviceOption: IEnableDebuggingDeviceOptions, debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise { - const liveSyncProcessInfo: ILiveSyncProcessInfo = this.liveSyncProcessesInfo[debuggingAdditionalOptions.projectDir]; - if (liveSyncProcessInfo && liveSyncProcessInfo.currentSyncAction) { - await liveSyncProcessInfo.currentSyncAction; - } - - return this.enableDebuggingCoreWithoutWaitingCurrentAction(deviceOption, debuggingAdditionalOptions); - } - - public disableDebugging(deviceOptions: IDisableDebuggingDeviceOptions[], debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise[] { - return _.map(deviceOptions, d => this.disableDebuggingCore(d, debuggingAdditionalOptions)); - } - - @hook('watchPatterns') - public async getWatcherPatterns(liveSyncData: ILiveSyncInfo, projectData: IProjectData, platforms: string[]): Promise { - // liveSyncData and platforms are used by plugins that make use of the watchPatterns hook - return [projectData.getAppDirectoryRelativePath(), projectData.getAppResourcesRelativeDirectoryPath()]; - } - - public async disableDebuggingCore(deviceOption: IDisableDebuggingDeviceOptions, debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise { - const liveSyncProcessInfo = this.liveSyncProcessesInfo[debuggingAdditionalOptions.projectDir]; - if (liveSyncProcessInfo.currentSyncAction) { - await liveSyncProcessInfo.currentSyncAction; - } - - const currentDeviceDescriptor = this.getDeviceDescriptor(deviceOption.deviceIdentifier, debuggingAdditionalOptions.projectDir); - if (currentDeviceDescriptor) { - currentDeviceDescriptor.debugggingEnabled = false; - } else { - this.$errors.failWithoutHelp(`Couldn't disable debugging for ${deviceOption.deviceIdentifier}`); - } - - const currentDevice = this.$devicesService.getDeviceByIdentifier(currentDeviceDescriptor.identifier); - if (!currentDevice) { - this.$errors.failWithoutHelp(`Couldn't disable debugging for ${deviceOption.deviceIdentifier}. Could not find device.`); - } - - await this.$debugService.debugStop(currentDevice.deviceInfo.identifier); - this.emit(DEBUGGER_DETACHED_EVENT_NAME, { deviceIdentifier: currentDeviceDescriptor.identifier }); - } - - @hook("liveSync") - private async liveSyncOperation(deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo, projectData: IProjectData): Promise { - let deviceDescriptorsForInitialSync: ILiveSyncDeviceInfo[] = []; - - if (liveSyncData.syncToPreviewApp) { - await this.$previewAppLiveSyncService.initialize({ - projectDir: projectData.projectDir, - bundle: liveSyncData.bundle, - useHotModuleReload: liveSyncData.useHotModuleReload, - env: liveSyncData.env - }); - } else { - // In case liveSync is called for a second time for the same projectDir. - const isAlreadyLiveSyncing = this.liveSyncProcessesInfo[projectData.projectDir] && !this.liveSyncProcessesInfo[projectData.projectDir].isStopped; - - // Prevent cases where liveSync is called consecutive times with the same device, for example [ A, B, C ] and then [ A, B, D ] - we want to execute initialSync only for D. - const currentlyRunningDeviceDescriptors = this.getLiveSyncDeviceDescriptors(projectData.projectDir); - deviceDescriptorsForInitialSync = isAlreadyLiveSyncing ? _.differenceBy(deviceDescriptors, currentlyRunningDeviceDescriptors, deviceDescriptorPrimaryKey) : deviceDescriptors; - } - - this.setLiveSyncProcessInfo(liveSyncData, deviceDescriptors); - - const shouldStartWatcher = !liveSyncData.skipWatcher && (liveSyncData.syncToPreviewApp || this.liveSyncProcessesInfo[projectData.projectDir].deviceDescriptors.length); - if (shouldStartWatcher) { - // Should be set after prepare - this.$usbLiveSyncService.isInitialized = true; - await this.startWatcher(projectData, liveSyncData, deviceDescriptors); - } - - await this.initialSync(projectData, liveSyncData, deviceDescriptorsForInitialSync); - } - - private setLiveSyncProcessInfo(liveSyncData: ILiveSyncInfo, deviceDescriptors: ILiveSyncDeviceInfo[]): void { - const { projectDir } = liveSyncData; - this.liveSyncProcessesInfo[projectDir] = this.liveSyncProcessesInfo[projectDir] || Object.create(null); - this.liveSyncProcessesInfo[projectDir].actionsChain = this.liveSyncProcessesInfo[projectDir].actionsChain || Promise.resolve(); - this.liveSyncProcessesInfo[projectDir].currentSyncAction = this.liveSyncProcessesInfo[projectDir].actionsChain; - this.liveSyncProcessesInfo[projectDir].isStopped = false; - this.liveSyncProcessesInfo[projectDir].syncToPreviewApp = liveSyncData.syncToPreviewApp; - - const currentDeviceDescriptors = this.getLiveSyncDeviceDescriptors(projectDir); - this.liveSyncProcessesInfo[projectDir].deviceDescriptors = _.uniqBy(currentDeviceDescriptors.concat(deviceDescriptors), deviceDescriptorPrimaryKey); - } - - private getLiveSyncService(platform: string): IPlatformLiveSyncService { - if (this.$mobileHelper.isiOSPlatform(platform)) { - return this.$injector.resolve("iOSLiveSyncService"); - } else if (this.$mobileHelper.isAndroidPlatform(platform)) { - return this.$injector.resolve("androidLiveSyncService"); - } - - this.$errors.failWithoutHelp(`Invalid platform ${platform}. Supported platforms are: ${this.$mobileHelper.platformNames.join(", ")}`); - } - - private async ensureLatestAppPackageIsInstalledOnDevice(options: IEnsureLatestAppPackageIsInstalledOnDeviceOptions, nativePrepare?: INativePrepare): Promise { - const platform = options.device.deviceInfo.platform; - const appInstalledOnDeviceResult: IAppInstalledOnDeviceResult = { appInstalled: false }; - if (options.preparedPlatforms.indexOf(platform) === -1) { - options.preparedPlatforms.push(platform); - - const platformSpecificOptions = options.deviceBuildInfoDescriptor.platformSpecificOptions || {}; - const prepareInfo: IPreparePlatformInfo = { - platform, - appFilesUpdaterOptions: { - bundle: options.bundle, - release: options.release, - watchAllFiles: options.liveSyncData.watchAllFiles, - useHotModuleReload: options.liveSyncData.useHotModuleReload - }, - projectData: options.projectData, - env: options.env, - nativePrepare: nativePrepare, - filesToSync: options.filesToSync, - filesToRemove: options.filesToRemove, - skipModulesNativeCheck: options.skipModulesNativeCheck, - config: platformSpecificOptions - }; - - await this.$platformService.preparePlatform(prepareInfo); - } - - const buildResult = await this.installedCachedAppPackage(platform, options); - if (buildResult) { - appInstalledOnDeviceResult.appInstalled = true; - return appInstalledOnDeviceResult; - } - - const shouldBuild = await this.$platformService.shouldBuild(platform, - options.projectData, - { buildForDevice: !options.device.isEmulator, clean: options.liveSyncData && options.liveSyncData.clean }, - options.deviceBuildInfoDescriptor.outputPath); - let pathToBuildItem = null; - if (shouldBuild) { - pathToBuildItem = await options.deviceBuildInfoDescriptor.buildAction(); - options.rebuiltInformation.push({ isEmulator: options.device.isEmulator, platform, pathToBuildItem }); - } else { - await this.$analyticsService.trackEventActionInGoogleAnalytics({ - action: TrackActionNames.LiveSync, - device: options.device, - projectDir: options.projectData.projectDir - }); - } - - await this.$platformService.validateInstall(options.device, options.projectData, options, options.deviceBuildInfoDescriptor.outputPath); - const shouldInstall = await this.$platformService.shouldInstall(options.device, options.projectData, options, options.deviceBuildInfoDescriptor.outputPath); - if (shouldInstall) { - const buildConfig = this.getInstallApplicationBuildConfig(options.device.deviceInfo.identifier, options.projectData.projectDir, { isEmulator: options.device.isEmulator }); - await this.$platformService.installApplication(options.device, buildConfig, options.projectData, pathToBuildItem, options.deviceBuildInfoDescriptor.outputPath); - appInstalledOnDeviceResult.appInstalled = true; - } - - return appInstalledOnDeviceResult; - } - - private async installedCachedAppPackage(platform: string, options: IEnsureLatestAppPackageIsInstalledOnDeviceOptions): Promise { - const rebuildInfo = _.find(options.rebuiltInformation, info => info.platform === platform && (this.$mobileHelper.isAndroidPlatform(platform) || info.isEmulator === options.device.isEmulator)); - - if (rebuildInfo) { - // Case where we have three devices attached, a change that requires build is found, - // we'll rebuild the app only for the first device, but we should install new package on all three devices. - const buildConfig = this.getInstallApplicationBuildConfig(options.device.deviceInfo.identifier, options.projectData.projectDir, { isEmulator: options.device.isEmulator }); - await this.$platformService.installApplication(options.device, buildConfig, options.projectData, rebuildInfo.pathToBuildItem, options.deviceBuildInfoDescriptor.outputPath); - return rebuildInfo.pathToBuildItem; - } - - return null; - } - - private async initialSync(projectData: IProjectData, liveSyncData: ILiveSyncInfo, deviceDescriptors: ILiveSyncDeviceInfo[]): Promise { - if (!liveSyncData.syncToPreviewApp) { - await this.initialCableSync(projectData, liveSyncData, deviceDescriptors); - } - } - - private async initialCableSync(projectData: IProjectData, liveSyncData: ILiveSyncInfo, deviceDescriptors: ILiveSyncDeviceInfo[]): Promise { - const preparedPlatforms: string[] = []; - const rebuiltInformation: ILiveSyncBuildInfo[] = []; - - const settings = this.getDefaultLatestAppPackageInstalledSettings(); - // Now fullSync - const deviceAction = async (device: Mobile.IDevice): Promise => { - const platform = device.deviceInfo.platform; - try { - const platformLiveSyncService = this.getLiveSyncService(platform); - - const deviceBuildInfoDescriptor = _.find(deviceDescriptors, dd => dd.identifier === device.deviceInfo.identifier); - - await this.ensureLatestAppPackageIsInstalledOnDevice({ - device, - preparedPlatforms, - rebuiltInformation, - projectData, - deviceBuildInfoDescriptor, - liveSyncData, - settings, - bundle: liveSyncData.bundle, - release: liveSyncData.release, - env: liveSyncData.env - }, { skipNativePrepare: deviceBuildInfoDescriptor.skipNativePrepare }); - - const liveSyncResultInfo = await platformLiveSyncService.fullSync({ - projectData, - device, - syncAllFiles: liveSyncData.watchAllFiles, - useHotModuleReload: liveSyncData.useHotModuleReload, - watch: !liveSyncData.skipWatcher, - force: liveSyncData.force, - liveSyncDeviceInfo: deviceBuildInfoDescriptor - }); - - await this.refreshApplication(projectData, liveSyncResultInfo, deviceBuildInfoDescriptor.debugOptions, deviceBuildInfoDescriptor.outputPath); - - this.$logger.info(`Successfully synced application ${liveSyncResultInfo.deviceAppData.appIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}.`); - - this.emitLivesyncEvent(LiveSyncEvents.liveSyncStarted, { - projectDir: projectData.projectDir, - deviceIdentifier: device.deviceInfo.identifier, - applicationIdentifier: projectData.projectIdentifiers[platform.toLowerCase()] - }); - } catch (err) { - this.$logger.warn(`Unable to apply changes on device: ${device.deviceInfo.identifier}. Error is: ${err.message}.`); - - this.emitLivesyncEvent(LiveSyncEvents.liveSyncError, { - error: err, - deviceIdentifier: device.deviceInfo.identifier, - projectDir: projectData.projectDir, - applicationIdentifier: projectData.projectIdentifiers[platform.toLowerCase()] - }); - - await this.stopLiveSync(projectData.projectDir, [device.deviceInfo.identifier], { shouldAwaitAllActions: false }); - } - }; - - // Execute the action only on the deviceDescriptors passed to initialSync. - // In case where we add deviceDescriptors to already running application, we've already executed initialSync for them. - await this.addActionToChain(projectData.projectDir, () => this.$devicesService.execute(deviceAction, (device: Mobile.IDevice) => _.some(deviceDescriptors, deviceDescriptor => deviceDescriptor.identifier === device.deviceInfo.identifier))); - - this.attachDeviceLostHandler(); - } - - private getDefaultLatestAppPackageInstalledSettings(): ILatestAppPackageInstalledSettings { - return { - [this.$devicePlatformsConstants.Android]: { - [DeviceTypes.Device]: false, - [DeviceTypes.Emulator]: false - }, - [this.$devicePlatformsConstants.iOS]: { - [DeviceTypes.Device]: false, - [DeviceTypes.Emulator]: false - } - }; - } - - private async startWatcher(projectData: IProjectData, liveSyncData: ILiveSyncInfo, deviceDescriptors: ILiveSyncDeviceInfo[]): Promise { - const devicesIds = deviceDescriptors.map(dd => dd.identifier); - const devices = _.filter(this.$devicesService.getDeviceInstances(), device => _.includes(devicesIds, device.deviceInfo.identifier)); - const platforms = _(devices).map(device => device.deviceInfo.platform).uniq().value(); - const patterns = await this.getWatcherPatterns(liveSyncData, projectData, platforms); - - if (liveSyncData.useHotModuleReload) { - this.$hmrStatusService.attachToHmrStatusEvent(); - } - - if (liveSyncData.watchAllFiles) { - const productionDependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir); - patterns.push(PACKAGE_JSON_FILE_NAME); - - // watch only production node_module/packages same one prepare uses - for (const index in productionDependencies) { - patterns.push(productionDependencies[index].directory); - } - } - - const currentWatcherInfo = this.liveSyncProcessesInfo[liveSyncData.projectDir].watcherInfo; - const areWatcherPatternsDifferent = () => _.xor(currentWatcherInfo.patterns, patterns).length; - if (!currentWatcherInfo || areWatcherPatternsDifferent()) { - if (currentWatcherInfo) { - currentWatcherInfo.watcher.close(); - } - - let filesToSync: string[] = []; - const hmrData: IDictionary = {}; - const filesToSyncMap: IDictionary = {}; - let filesToRemove: string[] = []; - let timeoutTimer: NodeJS.Timer; - - const startSyncFilesTimeout = (platform?: string, opts?: { calledFromHook: boolean }) => { - timeoutTimer = setTimeout(async () => { - if (platform && liveSyncData.bundle) { - filesToSync = filesToSyncMap[platform]; - } - - if (filesToSync.length || filesToRemove.length) { - const currentFilesToSync = _.cloneDeep(filesToSync); - filesToSync.splice(0, filesToSync.length); - - const currentFilesToRemove = _.cloneDeep(filesToRemove); - filesToRemove = []; - - if (liveSyncData.syncToPreviewApp) { - await this.addActionToChain(projectData.projectDir, async () => { - await this.$previewAppLiveSyncService.syncFiles({ - projectDir: projectData.projectDir, - bundle: liveSyncData.bundle, - useHotModuleReload: liveSyncData.useHotModuleReload, - env: liveSyncData.env - }, currentFilesToSync, currentFilesToRemove); - }); - } else { - // Push actions to the queue, do not start them simultaneously - await this.addActionToChain(projectData.projectDir, async () => { - try { - const currentHmrData = _.cloneDeep(hmrData); - - const allModifiedFiles = [].concat(currentFilesToSync).concat(currentFilesToRemove); - - const preparedPlatforms: string[] = []; - const rebuiltInformation: ILiveSyncBuildInfo[] = []; - - const latestAppPackageInstalledSettings = this.getDefaultLatestAppPackageInstalledSettings(); - - await this.$devicesService.execute(async (device: Mobile.IDevice) => { - const liveSyncProcessInfo = this.liveSyncProcessesInfo[projectData.projectDir]; - const deviceBuildInfoDescriptor = _.find(liveSyncProcessInfo.deviceDescriptors, dd => dd.identifier === device.deviceInfo.identifier); - const platformHmrData = (currentHmrData && currentHmrData[device.deviceInfo.platform]) || {}; - - const settings: ILiveSyncWatchInfo = { - liveSyncDeviceInfo: deviceBuildInfoDescriptor, - projectData, - filesToRemove: currentFilesToRemove, - filesToSync: currentFilesToSync, - isReinstalled: false, - syncAllFiles: liveSyncData.watchAllFiles, - hmrData: platformHmrData, - useHotModuleReload: liveSyncData.useHotModuleReload, - force: liveSyncData.force, - connectTimeout: 1000 - }; - - const service = this.getLiveSyncService(device.deviceInfo.platform); - - const watchAction = async (watchInfo: ILiveSyncWatchInfo): Promise => { - const isInHMRMode = liveSyncData.useHotModuleReload && platformHmrData.hash; - if (isInHMRMode) { - this.$hmrStatusService.watchHmrStatus(device.deviceInfo.identifier, platformHmrData.hash); - } - - let liveSyncResultInfo = await service.liveSyncWatchAction(device, watchInfo); - - await this.refreshApplication(projectData, liveSyncResultInfo, deviceBuildInfoDescriptor.debugOptions, deviceBuildInfoDescriptor.outputPath); - - // If didRecover is true, this means we were in ErrorActivity and fallback files were already transferred and app will be restarted. - if (!liveSyncResultInfo.didRecover && isInHMRMode) { - const status = await this.$hmrStatusService.getHmrStatus(device.deviceInfo.identifier, platformHmrData.hash); - if (status === HmrConstants.HMR_ERROR_STATUS) { - watchInfo.filesToSync = platformHmrData.fallbackFiles; - liveSyncResultInfo = await service.liveSyncWatchAction(device, watchInfo); - // We want to force a restart of the application. - liveSyncResultInfo.isFullSync = true; - await this.refreshApplication(projectData, liveSyncResultInfo, deviceBuildInfoDescriptor.debugOptions, deviceBuildInfoDescriptor.outputPath); - } - } - - this.$logger.info(`Successfully synced application ${liveSyncResultInfo.deviceAppData.appIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}.`); - }; - - if (liveSyncData.useHotModuleReload && opts && opts.calledFromHook) { - try { - this.$logger.trace("Try executing watch action without any preparation of files."); - await watchAction(settings); - this.$logger.trace("Successfully executed watch action without any preparation of files."); - return; - } catch (err) { - this.$logger.trace(`Error while trying to execute fast sync. Now we'll check the state of the app and we'll try to resurrect from the error. The error is: ${err}`); - } - } - - const appInstalledOnDeviceResult = await this.ensureLatestAppPackageIsInstalledOnDevice({ - device, - preparedPlatforms, - rebuiltInformation, - projectData, - deviceBuildInfoDescriptor, - // the clean option should be respected only during initial sync - liveSyncData: _.assign({}, liveSyncData, { clean: false }), - settings: latestAppPackageInstalledSettings, - modifiedFiles: allModifiedFiles, - filesToRemove: currentFilesToRemove, - filesToSync: currentFilesToSync, - bundle: liveSyncData.bundle, - release: liveSyncData.release, - env: liveSyncData.env, - skipModulesNativeCheck: !liveSyncData.watchAllFiles - }, { skipNativePrepare: deviceBuildInfoDescriptor.skipNativePrepare }); - - settings.isReinstalled = appInstalledOnDeviceResult.appInstalled; - settings.connectTimeout = null; - - if (liveSyncData.useHotModuleReload && appInstalledOnDeviceResult.appInstalled) { - _.each(platformHmrData.fallbackFiles, fileToSync => currentFilesToSync.push(fileToSync)); - } - - await watchAction(settings); - }, - (device: Mobile.IDevice) => { - const liveSyncProcessInfo = this.liveSyncProcessesInfo[projectData.projectDir]; - return (!platform || platform.toLowerCase() === device.deviceInfo.platform.toLowerCase()) && liveSyncProcessInfo && _.some(liveSyncProcessInfo.deviceDescriptors, deviceDescriptor => deviceDescriptor.identifier === device.deviceInfo.identifier); - } - ); - } catch (err) { - const allErrors = (err).allErrors; - - if (allErrors && _.isArray(allErrors)) { - for (const deviceError of allErrors) { - this.$logger.warn(`Unable to apply changes for device: ${deviceError.deviceIdentifier}. Error is: ${deviceError.message}.`); - const device = this.$devicesService.getDeviceByIdentifier(deviceError.deviceIdentifier); - this.emitLivesyncEvent(LiveSyncEvents.liveSyncError, { - error: deviceError, - deviceIdentifier: deviceError.deviceIdentifier, - projectDir: projectData.projectDir, - applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()] - }); - - await this.stopLiveSync(projectData.projectDir, [deviceError.deviceIdentifier], { shouldAwaitAllActions: false }); - } - } - } - }); - } - } - }, liveSyncData.useHotModuleReload ? 0 : 250); - - this.liveSyncProcessesInfo[liveSyncData.projectDir].timer = timeoutTimer; - }; - - await this.$hooksService.executeBeforeHooks('watch', { - hookArgs: { - projectData, - config: { - env: liveSyncData.env, - appFilesUpdaterOptions: { - bundle: liveSyncData.bundle, - release: liveSyncData.release, - watchAllFiles: liveSyncData.watchAllFiles, - useHotModuleReload: liveSyncData.useHotModuleReload - }, - platforms - }, - filesToSync, - filesToSyncMap, - hmrData, - filesToRemove, - startSyncFilesTimeout: async (platform: string) => { - const opts = { calledFromHook: true }; - if (platform) { - await startSyncFilesTimeout(platform, opts); - } else { - // This code is added for backwards compatibility with old versions of nativescript-dev-webpack plugin. - await startSyncFilesTimeout(null, opts); - } - } - } - }); - - const watcherOptions: choki.WatchOptions = { - ignoreInitial: true, - cwd: liveSyncData.projectDir, - awaitWriteFinish: { - pollInterval: 100, - stabilityThreshold: 500 - }, - ignored: ["**/.*", ".*"] // hidden files - }; - - const watcher = choki.watch(patterns, watcherOptions) - .on("all", async (event: string, filePath: string) => { - - clearTimeout(timeoutTimer); - - filePath = path.join(liveSyncData.projectDir, filePath); - - this.$logger.trace(`Chokidar raised event ${event} for ${filePath}.`); - - if (event === "add" || event === "addDir" || event === "change" /* <--- what to do when change event is raised ? */) { - filesToSync.push(filePath); - } else if (event === "unlink" || event === "unlinkDir") { - filesToRemove.push(filePath); - } - - startSyncFilesTimeout(); - }); - - this.liveSyncProcessesInfo[liveSyncData.projectDir].watcherInfo = { watcher, patterns }; - this.liveSyncProcessesInfo[liveSyncData.projectDir].timer = timeoutTimer; - } - } - - @cache() - private attachDeviceLostHandler(): void { - this.$devicesService.on(DeviceDiscoveryEventNames.DEVICE_LOST, async (device: Mobile.IDevice) => { - this.$logger.trace(`Received ${DeviceDiscoveryEventNames.DEVICE_LOST} event in LiveSync service for ${device.deviceInfo.identifier}. Will stop LiveSync operation for this device.`); - - for (const projectDir in this.liveSyncProcessesInfo) { - try { - if (_.find(this.liveSyncProcessesInfo[projectDir].deviceDescriptors, d => d.identifier === device.deviceInfo.identifier)) { - await this.stopLiveSync(projectDir, [device.deviceInfo.identifier]); - } - } catch (err) { - this.$logger.warn(`Unable to stop LiveSync operation for ${device.deviceInfo.identifier}.`, err); - } - } - }); - } - - private async addActionToChain(projectDir: string, action: () => Promise): Promise { - const liveSyncInfo = this.liveSyncProcessesInfo[projectDir]; - if (liveSyncInfo) { - liveSyncInfo.actionsChain = liveSyncInfo.actionsChain.then(async () => { - if (!liveSyncInfo.isStopped) { - liveSyncInfo.currentSyncAction = action(); - const res = await liveSyncInfo.currentSyncAction; - return res; - } - }); - - const result = await liveSyncInfo.actionsChain; - return result; - } - } - - private getInstallApplicationBuildConfig(deviceIdentifier: string, projectDir: string, opts: { isEmulator: boolean }): IBuildConfig { - const buildConfig: IBuildConfig = { - buildForDevice: !opts.isEmulator, - iCloudContainerEnvironment: null, - release: false, - device: deviceIdentifier, - provision: null, - teamId: null, - projectDir - }; - - return buildConfig; - } - - public emitLivesyncEvent(event: string, livesyncData: ILiveSyncEventData): boolean { - this.$logger.trace(`Will emit event ${event} with data`, livesyncData); - return this.emit(event, livesyncData); - } - -} - -$injector.register("liveSyncService", LiveSyncService); - -/** - * This class is used only for old versions of nativescript-dev-typescript plugin. - * It should be replaced with liveSyncService.isInitalized. - * Consider adding get and set methods for isInitialized, - * so whenever someone tries to access the value of isInitialized, - * they'll get a warning to update the plugins (like nativescript-dev-typescript). - */ -export class DeprecatedUsbLiveSyncService { - public isInitialized = false; -} - -$injector.register("usbLiveSyncService", DeprecatedUsbLiveSyncService); diff --git a/lib/services/livesync/platform-livesync-service-base.ts b/lib/services/livesync/platform-livesync-service-base.ts index fac12ac588..e3e3b6e615 100644 --- a/lib/services/livesync/platform-livesync-service-base.ts +++ b/lib/services/livesync/platform-livesync-service-base.ts @@ -8,14 +8,13 @@ export abstract class PlatformLiveSyncServiceBase { constructor(protected $fs: IFileSystem, protected $logger: ILogger, - protected $platformsData: IPlatformsData, + protected $platformsDataService: IPlatformsDataService, protected $projectFilesManager: IProjectFilesManager, - private $devicePathProvider: IDevicePathProvider, - private $projectFilesProvider: IProjectFilesProvider) { } + private $devicePathProvider: IDevicePathProvider) { } public getDeviceLiveSyncService(device: Mobile.IDevice, projectData: IProjectData): INativeScriptDeviceLiveSyncService { const platform = device.deviceInfo.platform.toLowerCase(); - const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); + const platformData = this.$platformsDataService.getPlatformData(device.deviceInfo.platform, projectData); const frameworkVersion = platformData.platformProjectService.getFrameworkVersion(projectData); const key = getHash(`${device.deviceInfo.identifier}${projectData.projectIdentifiers[platform]}${projectData.projectDir}${frameworkVersion}`); if (!this._deviceLiveSyncServicesCache[key]) { @@ -54,7 +53,7 @@ export abstract class PlatformLiveSyncServiceBase { const projectData = syncInfo.projectData; const device = syncInfo.device; const deviceLiveSyncService = this.getDeviceLiveSyncService(device, syncInfo.projectData); - const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); + const platformData = this.$platformsDataService.getPlatformData(device.deviceInfo.platform, projectData); const deviceAppData = await this.getAppData(syncInfo); if (deviceLiveSyncService.beforeLiveSyncAction) { @@ -69,7 +68,7 @@ export abstract class PlatformLiveSyncServiceBase { modifiedFilesData, isFullSync: true, deviceAppData, - useHotModuleReload: syncInfo.useHotModuleReload + useHotModuleReload: syncInfo.useHotModuleReload, }; } @@ -86,18 +85,18 @@ export abstract class PlatformLiveSyncServiceBase { let modifiedLocalToDevicePaths: Mobile.ILocalToDevicePathData[] = []; if (liveSyncInfo.filesToSync.length) { const filesToSync = liveSyncInfo.filesToSync; - const mappedFiles = _.map(filesToSync, filePath => this.$projectFilesProvider.mapFilePath(filePath, device.deviceInfo.platform, projectData)); + // const mappedFiles = _.map(filesToSync, filePath => this.$projectFilesProvider.mapFilePath(filePath, device.deviceInfo.platform, projectData)); // Some plugins modify platforms dir on afterPrepare (check nativescript-dev-sass) - we want to sync only existing file. - const existingFiles = mappedFiles.filter(m => m && this.$fs.exists(m)); + const existingFiles = filesToSync.filter(m => m && this.$fs.exists(m)); this.$logger.trace("Will execute livesync for files: ", existingFiles); - const skippedFiles = _.difference(mappedFiles, existingFiles); + const skippedFiles = _.difference(filesToSync, existingFiles); if (skippedFiles.length) { this.$logger.trace("The following files will not be synced as they do not exist:", skippedFiles); } if (existingFiles.length) { - const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); + const platformData = this.$platformsDataService.getPlatformData(device.deviceInfo.platform, projectData); const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); const localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, existingFiles, []); @@ -108,10 +107,10 @@ export abstract class PlatformLiveSyncServiceBase { if (liveSyncInfo.filesToRemove.length) { const filePaths = liveSyncInfo.filesToRemove; - const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); + const platformData = this.$platformsDataService.getPlatformData(device.deviceInfo.platform, projectData); const mappedFiles = _(filePaths) - .map(filePath => this.$projectFilesProvider.mapFilePath(filePath, device.deviceInfo.platform, projectData)) + // .map(filePath => this.$projectFilesProvider.mapFilePath(filePath, device.deviceInfo.platform, projectData)) .filter(filePath => !!filePath) .value(); const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); diff --git a/lib/services/livesync/playground/preview-app-files-service.ts b/lib/services/livesync/playground/preview-app-files-service.ts index 1c39d4acfe..5237790b66 100644 --- a/lib/services/livesync/playground/preview-app-files-service.ts +++ b/lib/services/livesync/playground/preview-app-files-service.ts @@ -11,7 +11,7 @@ export class PreviewAppFilesService implements IPreviewAppFilesService { constructor( private $fs: IFileSystem, private $logger: ILogger, - private $platformsData: IPlatformsData, + private $platformsDataService: IPlatformsDataService, private $projectDataService: IProjectDataService, private $projectFilesManager: IProjectFilesManager, private $projectFilesProvider: IProjectFilesProvider @@ -84,14 +84,8 @@ export class PreviewAppFilesService implements IPreviewAppFilesService { private getRootFilesDir(data: IPreviewAppLiveSyncData, platform: string): string { const projectData = this.$projectDataService.getProjectData(data.projectDir); - const platformData = this.$platformsData.getPlatformData(platform, projectData); - - let rootFilesDir = null; - if (data.bundle) { - rootFilesDir = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); - } else { - rootFilesDir = projectData.getAppDirectoryPath(); - } + const platformData = this.$platformsDataService.getPlatformData(platform, projectData); + const rootFilesDir = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); return rootFilesDir; } diff --git a/lib/services/livesync/playground/preview-app-livesync-service.ts b/lib/services/livesync/playground/preview-app-livesync-service.ts index ad59828b55..66e0e1ac84 100644 --- a/lib/services/livesync/playground/preview-app-livesync-service.ts +++ b/lib/services/livesync/playground/preview-app-livesync-service.ts @@ -1,69 +1,18 @@ -import * as path from "path"; -import { Device, FilesPayload } from "nativescript-preview-sdk"; -import { APP_RESOURCES_FOLDER_NAME, APP_FOLDER_NAME, TrackActionNames } from "../../../constants"; -import { PreviewAppLiveSyncEvents } from "./preview-app-constants"; -import { HmrConstants } from "../../../common/constants"; -import { stringify } from "../../../common/helpers"; +import { APP_RESOURCES_FOLDER_NAME } from "../../../constants"; import { EventEmitter } from "events"; import { performanceLog } from "../../../common/decorators"; +import { PreviewAppEmitter } from "../../../emitters/preview-app-emitter"; export class PreviewAppLiveSyncService extends EventEmitter implements IPreviewAppLiveSyncService { - private deviceInitializationPromise: IDictionary> = {}; - constructor( - private $analyticsService: IAnalyticsService, - private $errors: IErrors, - private $hooksService: IHooksService, private $logger: ILogger, - private $platformsData: IPlatformsData, - private $projectDataService: IProjectDataService, - private $previewSdkService: IPreviewSdkService, + private $previewAppEmitter: PreviewAppEmitter, private $previewAppFilesService: IPreviewAppFilesService, private $previewAppPluginsService: IPreviewAppPluginsService, private $previewDevicesService: IPreviewDevicesService, - private $hmrStatusService: IHmrStatusService) { - super(); - } - - @performanceLog() - public async initialize(data: IPreviewAppLiveSyncData): Promise { - await this.$previewSdkService.initialize(data.projectDir, async (device: Device) => { - try { - if (!device) { - this.$errors.failWithoutHelp("Sending initial preview files without a specified device is not supported."); - } - - if (this.deviceInitializationPromise[device.id]) { - return this.deviceInitializationPromise[device.id]; - } - - if (device.uniqueId) { - await this.$analyticsService.trackEventActionInGoogleAnalytics({ - action: TrackActionNames.PreviewAppData, - platform: device.platform, - additionalData: device.uniqueId - }); - } - - this.deviceInitializationPromise[device.id] = this.getInitialFilesForDevice(data, device); - try { - const payloads = await this.deviceInitializationPromise[device.id]; - return payloads; - } finally { - this.deviceInitializationPromise[device.id] = null; - } - } catch (error) { - this.$logger.trace(`Error while sending files on device ${device && device.id}. Error is`, error); - this.emit(PreviewAppLiveSyncEvents.PREVIEW_APP_LIVE_SYNC_ERROR, { - error, - data, - platform: device.platform, - deviceId: device.id - }); - } - }); - } + private $previewSdkService: IPreviewSdkService, + ) { super(); } @performanceLog() public async syncFiles(data: IPreviewAppLiveSyncData, filesToSync: string[], filesToRemove: string[]): Promise { @@ -84,56 +33,7 @@ export class PreviewAppLiveSyncService extends EventEmitter implements IPreviewA } } - public async stopLiveSync(): Promise { - this.$previewSdkService.stop(); - this.$previewDevicesService.updateConnectedDevices([]); - } - - private async getInitialFilesForDevice(data: IPreviewAppLiveSyncData, device: Device): Promise { - const hookArgs = this.getHookArgs(data, device); - await this.$hooksService.executeBeforeHooks("preview-sync", { hookArgs }); - await this.$previewAppPluginsService.comparePluginsOnDevice(data, device); - const payloads = await this.getInitialFilesForPlatformSafe(data, device.platform); - return payloads; - } - - private getHookArgs(data: IPreviewAppLiveSyncData, device: Device) { - const filesToSyncMap: IDictionary = {}; - const hmrData: IDictionary = {}; - const promise = Promise.resolve(); - const result = { - projectData: this.$projectDataService.getProjectData(data.projectDir), - hmrData, - config: { - env: data.env, - platform: device.platform, - appFilesUpdaterOptions: { - bundle: data.bundle, - useHotModuleReload: data.useHotModuleReload, - release: false - }, - }, - externals: this.$previewAppPluginsService.getExternalPlugins(device), - filesToSyncMap, - startSyncFilesTimeout: async (platform: string) => await this.onWebpackCompilationComplete(data, hmrData, filesToSyncMap, promise, platform) - }; - - return result; - } - - private async getInitialFilesForPlatformSafe(data: IPreviewAppLiveSyncData, platform: string): Promise { - this.$logger.info(`Start sending initial files for platform ${platform}.`); - - try { - const payloads = this.$previewAppFilesService.getInitialFilesPayload(data, platform); - this.$logger.info(`Successfully sent initial files for platform ${platform}.`); - return payloads; - } catch (err) { - this.$logger.warn(`Unable to apply changes for platform ${platform}. Error is: ${err}, ${stringify(err)}`); - } - } - - private async syncFilesForPlatformSafe(data: IPreviewAppLiveSyncData, filesData: IPreviewAppFilesData, platform: string, deviceId?: string): Promise { + public async syncFilesForPlatformSafe(data: IPreviewAppLiveSyncData, filesData: IPreviewAppFilesData, platform: string, deviceId?: string): Promise { try { const payloads = this.$previewAppFilesService.getFilesPayload(data, filesData, platform); if (payloads && payloads.files && payloads.files.length) { @@ -143,49 +43,10 @@ export class PreviewAppLiveSyncService extends EventEmitter implements IPreviewA } } catch (error) { this.$logger.warn(`Unable to apply changes for platform ${platform}. Error is: ${error}, ${JSON.stringify(error, null, 2)}.`); - this.emit(PreviewAppLiveSyncEvents.PREVIEW_APP_LIVE_SYNC_ERROR, { - error, - data, - platform, - deviceId - }); + this.$previewAppEmitter.emitPreviewAppLiveSyncError(data, deviceId, error); } } - @performanceLog() - private async onWebpackCompilationComplete(data: IPreviewAppLiveSyncData, hmrData: IDictionary, filesToSyncMap: IDictionary, promise: Promise, platform: string) { - await promise - .then(async () => { - const currentHmrData = _.cloneDeep(hmrData); - const platformHmrData = currentHmrData[platform] || {}; - const projectData = this.$projectDataService.getProjectData(data.projectDir); - const platformData = this.$platformsData.getPlatformData(platform, projectData); - const clonedFiles = _.cloneDeep(filesToSyncMap[platform]); - const filesToSync = _.map(clonedFiles, fileToSync => { - const result = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME, path.relative(projectData.getAppDirectoryPath(), fileToSync)); - return result; - }); - - promise = this.syncFilesForPlatformSafe(data, { filesToSync }, platform); - await promise; - - if (data.useHotModuleReload && platformHmrData.hash) { - const devices = this.$previewDevicesService.getDevicesForPlatform(platform); - - await Promise.all(_.map(devices, async (previewDevice: Device) => { - const status = await this.$hmrStatusService.getHmrStatus(previewDevice.id, platformHmrData.hash); - if (status === HmrConstants.HMR_ERROR_STATUS) { - const originalUseHotModuleReload = data.useHotModuleReload; - data.useHotModuleReload = false; - await this.syncFilesForPlatformSafe(data, { filesToSync: platformHmrData.fallbackFiles }, platform, previewDevice.id ); - data.useHotModuleReload = originalUseHotModuleReload; - } - })); - } - }); - filesToSyncMap[platform] = []; - } - private showWarningsForNativeFiles(files: string[]): void { _.filter(files, file => file.indexOf(APP_RESOURCES_FOLDER_NAME) > -1) .forEach(file => this.$logger.warn(`Unable to apply changes from ${APP_RESOURCES_FOLDER_NAME} folder. You need to build your application in order to make changes in ${APP_RESOURCES_FOLDER_NAME} folder.`)); diff --git a/lib/services/livesync/playground/preview-app-plugins-service.ts b/lib/services/livesync/playground/preview-app-plugins-service.ts index 1c52a6ae48..490c10e42e 100644 --- a/lib/services/livesync/playground/preview-app-plugins-service.ts +++ b/lib/services/livesync/playground/preview-app-plugins-service.ts @@ -69,15 +69,11 @@ export class PreviewAppPluginsService implements IPreviewAppPluginsService { } private getWarningForPlugin(data: IPreviewAppLiveSyncData, localPlugin: string, localPluginVersion: string, devicePluginVersion: string, device: Device): string { - if (data && data.bundle) { - const pluginPackageJsonPath = path.join(data.projectDir, NODE_MODULES_DIR_NAME, localPlugin, PACKAGE_JSON_FILE_NAME); - const isNativeScriptPlugin = this.$pluginsService.isNativeScriptPlugin(pluginPackageJsonPath); - if (!isNativeScriptPlugin || (isNativeScriptPlugin && !this.hasNativeCode(localPlugin, device.platform, data.projectDir))) { - return null; - } - } + const pluginPackageJsonPath = path.join(data.projectDir, NODE_MODULES_DIR_NAME, localPlugin, PACKAGE_JSON_FILE_NAME); + const isNativeScriptPlugin = this.$pluginsService.isNativeScriptPlugin(pluginPackageJsonPath); + const shouldCompare = isNativeScriptPlugin && this.hasNativeCode(localPlugin, device.platform, data.projectDir); - return this.getWarningForPluginCore(localPlugin, localPluginVersion, devicePluginVersion, device.id); + return shouldCompare ? this.getWarningForPluginCore(localPlugin, localPluginVersion, devicePluginVersion, device.id) : null; } private getWarningForPluginCore(localPlugin: string, localPluginVersion: string, devicePluginVersion: string, deviceId: string): string { diff --git a/lib/services/livesync/playground/preview-qr-code-service.ts b/lib/services/livesync/playground/preview-qr-code-service.ts index 2609a2cd55..3da28180c1 100644 --- a/lib/services/livesync/playground/preview-qr-code-service.ts +++ b/lib/services/livesync/playground/preview-qr-code-service.ts @@ -44,6 +44,8 @@ export class PreviewQrCodeService implements IPreviewQrCodeService { const qrCodeUrl = this.$previewSdkService.getQrCodeUrl(options); const url = await this.getShortenUrl(qrCodeUrl); + this.$logger.info("======== qrCodeUrl ======== ", qrCodeUrl); + this.$logger.info(); const message = `${EOL} Generating qrcode for url ${url}.`; this.$logger.trace(message); diff --git a/lib/services/local-build-service.ts b/lib/services/local-build-service.ts deleted file mode 100644 index ae50dfb717..0000000000 --- a/lib/services/local-build-service.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { EventEmitter } from "events"; -import { BUILD_OUTPUT_EVENT_NAME, ANDROID_RELEASE_BUILD_ERROR_MESSAGE } from "../constants"; -import { attachAwaitDetach } from "../common/helpers"; - -export class LocalBuildService extends EventEmitter implements ILocalBuildService { - constructor(private $projectData: IProjectData, - private $mobileHelper: Mobile.IMobileHelper, - private $errors: IErrors, - private $platformsData: IPlatformsData, - private $platformService: IPlatformService, - private $projectDataService: IProjectDataService) { - super(); - } - - public async build(platform: string, platformBuildOptions: IPlatformBuildData): Promise { - if (this.$mobileHelper.isAndroidPlatform(platform) && platformBuildOptions.release && (!platformBuildOptions.keyStorePath || !platformBuildOptions.keyStorePassword || !platformBuildOptions.keyStoreAlias || !platformBuildOptions.keyStoreAliasPassword)) { - this.$errors.fail(ANDROID_RELEASE_BUILD_ERROR_MESSAGE); - } - - this.$projectData.initializeProjectData(platformBuildOptions.projectDir); - const prepareInfo: IPreparePlatformInfo = { - platform, - appFilesUpdaterOptions: platformBuildOptions, - projectData: this.$projectData, - env: platformBuildOptions.env, - config: { - provision: platformBuildOptions.provision, - teamId: platformBuildOptions.teamId, - sdk: null, - frameworkPath: null, - ignoreScripts: false - } - }; - - await this.$platformService.preparePlatform(prepareInfo); - const handler = (data: any) => { - data.projectDir = platformBuildOptions.projectDir; - this.emit(BUILD_OUTPUT_EVENT_NAME, data); - }; - platformBuildOptions.buildOutputStdio = "pipe"; - - await attachAwaitDetach(BUILD_OUTPUT_EVENT_NAME, this.$platformService, handler, this.$platformService.buildPlatform(platform, platformBuildOptions, this.$projectData)); - return this.$platformService.lastOutputPath(platform, platformBuildOptions, this.$projectData); - } - - public async cleanNativeApp(data: ICleanNativeAppData): Promise { - const projectData = this.$projectDataService.getProjectData(data.projectDir); - const platformData = this.$platformsData.getPlatformData(data.platform, projectData); - await platformData.platformProjectService.cleanProject(platformData.projectRoot, projectData); - } -} - -$injector.register("localBuildService", LocalBuildService); diff --git a/lib/services/platform-environment-requirements.ts b/lib/services/platform-environment-requirements.ts index e30353eded..c7cf8f52c6 100644 --- a/lib/services/platform-environment-requirements.ts +++ b/lib/services/platform-environment-requirements.ts @@ -1,7 +1,6 @@ import { NATIVESCRIPT_CLOUD_EXTENSION_NAME, TrackActionNames } from "../constants"; import { isInteractive } from "../common/helpers"; import { EOL } from "os"; -import { cache } from "../common/decorators"; export class PlatformEnvironmentRequirements implements IPlatformEnvironmentRequirements { constructor(private $commandsService: ICommandsService, @@ -12,14 +11,9 @@ export class PlatformEnvironmentRequirements implements IPlatformEnvironmentRequ private $prompter: IPrompter, private $staticConfig: IStaticConfig, private $analyticsService: IAnalyticsService, - private $injector: IInjector, + // private $previewAppLiveSyncService: IPreviewAppLiveSyncService, private $previewQrCodeService: IPreviewQrCodeService) { } - @cache() - private get $liveSyncService(): ILiveSyncService { - return this.$injector.resolve("liveSyncService"); - } - public static CLOUD_SETUP_OPTION_NAME = "Configure for Cloud Builds"; public static LOCAL_SETUP_OPTION_NAME = "Configure for Local Builds"; public static TRY_CLOUD_OPERATION_OPTION_NAME = "Try Cloud Operation"; @@ -181,18 +175,12 @@ export class PlatformEnvironmentRequirements implements IPlatformEnvironmentRequ this.$errors.failWithoutHelp(`No project found. In order to sync to playground you need to go to project directory or specify --path option.`); } - await this.$liveSyncService.liveSync([], { - syncToPreviewApp: true, - projectDir, - skipWatcher: !options.watch, - watchAllFiles: options.syncAllFiles, - clean: options.clean, - bundle: !!options.bundle, - release: options.release, - env: options.env, - timeout: options.timeout, - useHotModuleReload: options.hmr - }); + // await this.$previewAppLiveSyncService.initialize({ + // projectDir, + // env: options.env, + // useHotModuleReload: options.hmr, + // bundle: true + // }); await this.$previewQrCodeService.printLiveSyncQrCode({ projectDir, useHotModuleReload: options.hmr, link: options.link }); } diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts deleted file mode 100644 index d6b935eca6..0000000000 --- a/lib/services/platform-service.ts +++ /dev/null @@ -1,975 +0,0 @@ -import * as path from "path"; -import * as shell from "shelljs"; -import * as constants from "../constants"; -import { Configurations } from "../common/constants"; -import * as helpers from "../common/helpers"; -import * as semver from "semver"; -import { format } from "util"; -import { EventEmitter } from "events"; -import { AppFilesUpdater } from "./app-files-updater"; -import { attachAwaitDetach } from "../common/helpers"; -import * as temp from "temp"; -import { performanceLog } from ".././common/decorators"; -temp.track(); - -const buildInfoFileName = ".nsbuildinfo"; - -export class PlatformService extends EventEmitter implements IPlatformService { - constructor(private $devicesService: Mobile.IDevicesService, - private $preparePlatformNativeService: IPreparePlatformService, - private $preparePlatformJSService: IPreparePlatformService, - private $errors: IErrors, - private $fs: IFileSystem, - private $logger: ILogger, - private $doctorService: IDoctorService, - private $packageInstallationManager: IPackageInstallationManager, - private $platformsData: IPlatformsData, - private $projectDataService: IProjectDataService, - private $pluginsService: IPluginsService, - private $projectFilesManager: IProjectFilesManager, - private $mobileHelper: Mobile.IMobileHelper, - private $hostInfo: IHostInfo, - private $devicePathProvider: IDevicePathProvider, - private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - private $projectChangesService: IProjectChangesService, - private $analyticsService: IAnalyticsService, - private $terminalSpinnerService: ITerminalSpinnerService, - private $pacoteService: IPacoteService, - private $usbLiveSyncService: any, - public $hooksService: IHooksService - ) { - super(); - } - - public async cleanPlatforms(platforms: string[], projectData: IProjectData, config: IPlatformOptions, framworkPath?: string): Promise { - for (const platform of platforms) { - const version: string = this.getCurrentPlatformVersion(platform, projectData); - - let platformWithVersion: string = platform; - if (version !== undefined) { - platformWithVersion += "@" + version; - } - - await this.removePlatforms([platform], projectData); - await this.addPlatforms([platformWithVersion], projectData, config); - } - } - - public async addPlatforms(platforms: string[], projectData: IProjectData, config: IPlatformOptions, frameworkPath?: string): Promise { - const platformsDir = projectData.platformsDir; - this.$fs.ensureDirectoryExists(platformsDir); - - for (const platform of platforms) { - this.validatePlatform(platform, projectData); - const platformPath = path.join(projectData.platformsDir, platform); - - const isPlatformAdded = this.isPlatformAdded(platform, platformPath, projectData); - if (isPlatformAdded) { - this.$errors.failWithoutHelp(`Platform ${platform} already added`); - } - - await this.addPlatform(platform.toLowerCase(), projectData, config, frameworkPath); - } - } - - public getCurrentPlatformVersion(platform: string, projectData: IProjectData): string { - const platformData = this.$platformsData.getPlatformData(platform, projectData); - const currentPlatformData: any = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName); - let version: string; - if (currentPlatformData && currentPlatformData[constants.VERSION_STRING]) { - version = currentPlatformData[constants.VERSION_STRING]; - } - - return version; - } - - private async addPlatform(platformParam: string, projectData: IProjectData, config: IPlatformOptions, frameworkPath?: string, nativePrepare?: INativePrepare): Promise { - const data = platformParam.split("@"); - const platform = data[0].toLowerCase(); - let version = data[1]; - - const platformData = this.$platformsData.getPlatformData(platform, projectData); - - // Log the values for project - this.$logger.trace("Creating NativeScript project for the %s platform", platform); - this.$logger.trace("Path: %s", platformData.projectRoot); - this.$logger.trace("Package: %s", projectData.projectIdentifiers[platform]); - this.$logger.trace("Name: %s", projectData.projectName); - - this.$logger.out("Copying template files..."); - - let packageToInstall = ""; - if (frameworkPath) { - packageToInstall = path.resolve(frameworkPath); - if (!this.$fs.exists(packageToInstall)) { - const errorMessage = format(constants.AddPlaformErrors.InvalidFrameworkPathStringFormat, frameworkPath); - this.$errors.fail(errorMessage); - } - } else { - if (!version) { - version = this.getCurrentPlatformVersion(platform, projectData) || - await this.$packageInstallationManager.getLatestCompatibleVersion(platformData.frameworkPackageName); - } - - packageToInstall = `${platformData.frameworkPackageName}@${version}`; - } - - const spinner = this.$terminalSpinnerService.createSpinner(); - const platformPath = path.join(projectData.platformsDir, platform); - let installedPlatformVersion; - - try { - spinner.start(); - const downloadedPackagePath = temp.mkdirSync("runtimeDir"); - temp.track(); - await this.$pacoteService.extractPackage(packageToInstall, downloadedPackagePath); - let frameworkDir = path.join(downloadedPackagePath, constants.PROJECT_FRAMEWORK_FOLDER_NAME); - frameworkDir = path.resolve(frameworkDir); - installedPlatformVersion = - await this.addPlatformCore(platformData, frameworkDir, projectData, config, nativePrepare); - } catch (err) { - this.$fs.deleteDirectory(platformPath); - throw err; - } finally { - spinner.stop(); - } - - this.$fs.ensureDirectoryExists(platformPath); - this.$logger.out(`Platform ${platform} successfully added. v${installedPlatformVersion}`); - } - - private async addPlatformCore(platformData: IPlatformData, frameworkDir: string, projectData: IProjectData, config: IPlatformOptions, nativePrepare?: INativePrepare): Promise { - const coreModuleData = this.$fs.readJson(path.join(frameworkDir, "..", "package.json")); - const installedVersion = coreModuleData.version; - - await this.$preparePlatformJSService.addPlatform({ - platformData, - frameworkDir, - installedVersion, - projectData, - config - }); - - if (!nativePrepare || !nativePrepare.skipNativePrepare) { - const platformDir = path.join(projectData.platformsDir, platformData.normalizedPlatformName.toLowerCase()); - this.$fs.deleteDirectory(platformDir); - await this.$preparePlatformNativeService.addPlatform({ - platformData, - frameworkDir, - installedVersion, - projectData, - config - }); - } - - return installedVersion; - } - - public getInstalledPlatforms(projectData: IProjectData): string[] { - if (!this.$fs.exists(projectData.platformsDir)) { - return []; - } - - const subDirs = this.$fs.readDirectory(projectData.platformsDir); - return _.filter(subDirs, p => this.$platformsData.platformsNames.indexOf(p) > -1); - } - - public getAvailablePlatforms(projectData: IProjectData): string[] { - const installedPlatforms = this.getInstalledPlatforms(projectData); - return _.filter(this.$platformsData.platformsNames, p => { - return installedPlatforms.indexOf(p) < 0 && this.isPlatformSupportedForOS(p, projectData); // Only those not already installed - }); - } - - public getPreparedPlatforms(projectData: IProjectData): string[] { - return _.filter(this.$platformsData.platformsNames, p => { return this.isPlatformPrepared(p, projectData); }); - } - - @performanceLog() - @helpers.hook('shouldPrepare') - public async shouldPrepare(shouldPrepareInfo: IShouldPrepareInfo): Promise { - shouldPrepareInfo.changesInfo = shouldPrepareInfo.changesInfo || await this.getChangesInfo(shouldPrepareInfo.platformInfo); - const requiresNativePrepare = (!shouldPrepareInfo.platformInfo.nativePrepare || !shouldPrepareInfo.platformInfo.nativePrepare.skipNativePrepare) && shouldPrepareInfo.changesInfo.nativePlatformStatus === constants.NativePlatformStatus.requiresPrepare; - - return shouldPrepareInfo.changesInfo.hasChanges || requiresNativePrepare; - } - - private async getChangesInfo(preparePlatformInfo: IPreparePlatformInfo): Promise { - await this.initialPrepare(preparePlatformInfo); - - const { platform, appFilesUpdaterOptions, projectData, config, nativePrepare } = preparePlatformInfo; - const bundle = appFilesUpdaterOptions.bundle; - const nativePlatformStatus = (nativePrepare && nativePrepare.skipNativePrepare) ? constants.NativePlatformStatus.requiresPlatformAdd : constants.NativePlatformStatus.requiresPrepare; - const changesInfo = await this.$projectChangesService.checkForChanges({ - platform, - projectData, - projectChangesOptions: { - bundle, - release: appFilesUpdaterOptions.release, - provision: config.provision, - teamId: config.teamId, - nativePlatformStatus, - skipModulesNativeCheck: preparePlatformInfo.skipModulesNativeCheck, - useHotModuleReload: appFilesUpdaterOptions.useHotModuleReload - } - }); - - this.$logger.trace("Changes info in prepare platform:", changesInfo); - return changesInfo; - } - - @performanceLog() - public async preparePlatform(platformInfo: IPreparePlatformInfo): Promise { - const changesInfo = await this.getChangesInfo(platformInfo); - const shouldPrepare = await this.shouldPrepare({ platformInfo, changesInfo }); - - if (shouldPrepare) { - // Always clear up the app directory in platforms if `--bundle` value has changed in between builds or is passed in general - // this is done as user has full control over what goes in platforms when `--bundle` is passed - // and we may end up with duplicate symbols which would fail the build - if (changesInfo.bundleChanged) { - await this.cleanDestinationApp(platformInfo); - } - - this.$doctorService.checkForDeprecatedShortImportsInAppDir(platformInfo.projectData.projectDir); - - await this.preparePlatformCore( - platformInfo.platform, - platformInfo.appFilesUpdaterOptions, - platformInfo.projectData, - platformInfo.config, - platformInfo.env, - changesInfo, - platformInfo.filesToSync, - platformInfo.filesToRemove, - platformInfo.nativePrepare - ); - this.$projectChangesService.savePrepareInfo(platformInfo.platform, platformInfo.projectData); - } else { - this.$logger.out("Skipping prepare."); - } - - return true; - } - - public async validateOptions(provision: true | string, teamId: true | string, projectData: IProjectData, platform?: string, aab?: boolean): Promise { - if (platform && !this.$mobileHelper.isAndroidPlatform(platform) && aab) { - this.$errors.failWithoutHelp("The --aab option is supported only for the Android platform."); - } - - if (platform) { - platform = this.$mobileHelper.normalizePlatformName(platform); - this.$logger.trace("Validate options for platform: " + platform); - const platformData = this.$platformsData.getPlatformData(platform, projectData); - - const result = await platformData.platformProjectService.validateOptions( - projectData.projectIdentifiers[platform.toLowerCase()], - provision, - teamId - ); - - return result; - } else { - let valid = true; - for (const availablePlatform in this.$platformsData.availablePlatforms) { - this.$logger.trace("Validate options for platform: " + availablePlatform); - const platformData = this.$platformsData.getPlatformData(availablePlatform, projectData); - valid = valid && await platformData.platformProjectService.validateOptions( - projectData.projectIdentifiers[availablePlatform.toLowerCase()], - provision, - teamId - ); - } - - return valid; - } - } - - private async initialPrepare(preparePlatformInfo: IPreparePlatformInfo) { - const { platform, appFilesUpdaterOptions, projectData, config, nativePrepare } = preparePlatformInfo; - this.validatePlatform(platform, projectData); - - // We need dev-dependencies here, so before-prepare hooks will be executed correctly. - try { - await this.$pluginsService.ensureAllDependenciesAreInstalled(projectData); - } catch (err) { - this.$logger.trace(err); - this.$errors.failWithoutHelp(`Unable to install dependencies. Make sure your package.json is valid and all dependencies are correct. Error is: ${err.message}`); - } - - await this.ensurePlatformInstalled(platform, projectData, config, appFilesUpdaterOptions, nativePrepare); - } - - /* Hooks are expected to use "filesToSync" parameter, as to give plugin authors additional information about the sync process.*/ - @performanceLog() - @helpers.hook('prepare') - private async preparePlatformCore(platform: string, - appFilesUpdaterOptions: IAppFilesUpdaterOptions, - projectData: IProjectData, - platformSpecificData: IPlatformSpecificData, - env: Object, - changesInfo?: IProjectChangesInfo, - filesToSync?: string[], - filesToRemove?: string[], - nativePrepare?: INativePrepare): Promise { - - this.$logger.out("Preparing project..."); - - const platformData = this.$platformsData.getPlatformData(platform, projectData); - const projectFilesConfig = helpers.getProjectFilesConfig({ isReleaseBuild: appFilesUpdaterOptions.release }); - await this.$preparePlatformJSService.preparePlatform({ - platform, - platformData, - projectFilesConfig, - appFilesUpdaterOptions, - projectData, - platformSpecificData, - changesInfo, - filesToSync, - filesToRemove, - env - }); - - if (!nativePrepare || !nativePrepare.skipNativePrepare) { - await this.$preparePlatformNativeService.preparePlatform({ - platform, - platformData, - appFilesUpdaterOptions, - projectData, - platformSpecificData, - changesInfo, - filesToSync, - filesToRemove, - projectFilesConfig, - env - }); - } - - const directoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); - const excludedDirs = [constants.APP_RESOURCES_FOLDER_NAME]; - if (!changesInfo || !changesInfo.modulesChanged) { - excludedDirs.push(constants.TNS_MODULES_FOLDER_NAME); - } - - this.$projectFilesManager.processPlatformSpecificFiles(directoryPath, platform, projectFilesConfig, excludedDirs); - - this.$logger.out(`Project successfully prepared (${platform})`); - } - - public async shouldBuild(platform: string, projectData: IProjectData, buildConfig: IBuildConfig, outputPath?: string): Promise { - if (buildConfig.release && this.$projectChangesService.currentChanges.hasChanges) { - return true; - } - - if (this.$projectChangesService.currentChanges.changesRequireBuild) { - return true; - } - - const platformData = this.$platformsData.getPlatformData(platform, projectData); - outputPath = outputPath || platformData.getBuildOutputPath(buildConfig); - if (!this.$fs.exists(outputPath)) { - return true; - } - - const validBuildOutputData = platformData.getValidBuildOutputData(buildConfig); - const packages = this.getApplicationPackages(outputPath, validBuildOutputData); - if (packages.length === 0) { - return true; - } - - const prepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData); - const buildInfo = this.getBuildInfo(platform, platformData, buildConfig, outputPath); - if (!prepareInfo || !buildInfo) { - return true; - } - - if (buildConfig.clean) { - return true; - } - - if (prepareInfo.time === buildInfo.prepareTime) { - return false; - } - - return prepareInfo.changesRequireBuildTime !== buildInfo.prepareTime; - } - - @performanceLog() - public async buildPlatform(platform: string, buildConfig: IBuildConfig, projectData: IProjectData): Promise { - this.$logger.out("Building project..."); - - const action = constants.TrackActionNames.Build; - const isForDevice = this.$mobileHelper.isAndroidPlatform(platform) ? null : buildConfig && buildConfig.buildForDevice; - - await this.$analyticsService.trackEventActionInGoogleAnalytics({ - action, - isForDevice, - platform, - projectDir: projectData.projectDir, - additionalData: `${buildConfig.release ? Configurations.Release : Configurations.Debug}_${buildConfig.clean ? constants.BuildStates.Clean : constants.BuildStates.Incremental}` - }); - - const platformData = this.$platformsData.getPlatformData(platform, projectData); - if (buildConfig.clean) { - await platformData.platformProjectService.cleanProject(platformData.projectRoot, projectData); - } - - const handler = (data: any) => { - this.emit(constants.BUILD_OUTPUT_EVENT_NAME, data); - this.$logger.printInfoMessageOnSameLine(data.data.toString()); - }; - - await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME, platformData.platformProjectService, handler, platformData.platformProjectService.buildProject(platformData.projectRoot, projectData, buildConfig)); - - const buildInfoFilePath = this.getBuildOutputPath(platform, platformData, buildConfig); - this.saveBuildInfoFile(platform, projectData.projectDir, buildInfoFilePath); - - this.$logger.out("Project successfully built."); - return this.lastOutputPath(platform, buildConfig, projectData); - } - - public saveBuildInfoFile(platform: string, projectDir: string, buildInfoFileDirname: string): void { - const buildInfoFile = path.join(buildInfoFileDirname, buildInfoFileName); - const projectData = this.$projectDataService.getProjectData(projectDir); - const platformData = this.$platformsData.getPlatformData(platform, projectData); - - const prepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData); - const buildInfo: IBuildInfo = { - prepareTime: prepareInfo.changesRequireBuildTime, - buildTime: new Date().toString() - }; - - const deploymentTarget = platformData.platformProjectService.getDeploymentTarget(projectData); - if (deploymentTarget) { - buildInfo.deploymentTarget = deploymentTarget.version; - } - - this.$fs.writeJson(buildInfoFile, buildInfo); - } - - public async shouldInstall(device: Mobile.IDevice, projectData: IProjectData, release: IRelease, outputPath?: string): Promise { - const platform = device.deviceInfo.platform; - if (!(await device.applicationManager.isApplicationInstalled(projectData.projectIdentifiers[platform.toLowerCase()]))) { - return true; - } - - const platformData = this.$platformsData.getPlatformData(platform, projectData); - const deviceBuildInfo: IBuildInfo = await this.getDeviceBuildInfo(device, projectData); - const localBuildInfo = this.getBuildInfo(platform, platformData, { buildForDevice: !device.isEmulator, release: release.release }, outputPath); - - return !localBuildInfo || !deviceBuildInfo || deviceBuildInfo.buildTime !== localBuildInfo.buildTime; - } - - public async validateInstall(device: Mobile.IDevice, projectData: IProjectData, release: IRelease, outputPath?: string): Promise { - const platform = device.deviceInfo.platform; - const platformData = this.$platformsData.getPlatformData(platform, projectData); - const localBuildInfo = this.getBuildInfo(device.deviceInfo.platform, platformData, { buildForDevice: !device.isEmulator, release: release.release }, outputPath); - if (localBuildInfo.deploymentTarget) { - if (semver.lt(semver.coerce(device.deviceInfo.version), semver.coerce(localBuildInfo.deploymentTarget))) { - this.$errors.fail(`Unable to install on device with version ${device.deviceInfo.version} as deployment target is ${localBuildInfo.deploymentTarget}`); - } - } - } - - public async installApplication(device: Mobile.IDevice, buildConfig: IBuildConfig, projectData: IProjectData, packageFile?: string, outputFilePath?: string): Promise { - this.$logger.out(`Installing on device ${device.deviceInfo.identifier}...`); - - await this.$analyticsService.trackEventActionInGoogleAnalytics({ - action: constants.TrackActionNames.Deploy, - device, - projectDir: projectData.projectDir - }); - - const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); - if (!packageFile) { - if (this.$devicesService.isiOSSimulator(device)) { - packageFile = this.getLatestApplicationPackageForEmulator(platformData, buildConfig, outputFilePath).packageName; - } else { - packageFile = this.getLatestApplicationPackageForDevice(platformData, buildConfig, outputFilePath).packageName; - } - } - - await platformData.platformProjectService.cleanDeviceTempFolder(device.deviceInfo.identifier, projectData); - - const platform = device.deviceInfo.platform.toLowerCase(); - await device.applicationManager.reinstallApplication(projectData.projectIdentifiers[platform], packageFile); - - await this.updateHashesOnDevice({ - device, - appIdentifier: projectData.projectIdentifiers[platform], - outputFilePath, - platformData - }); - - if (!buildConfig.release) { - const deviceFilePath = await this.getDeviceBuildInfoFilePath(device, projectData); - const options = buildConfig; - options.buildForDevice = !device.isEmulator; - const buildInfoFilePath = outputFilePath || this.getBuildOutputPath(device.deviceInfo.platform, platformData, buildConfig); - const appIdentifier = projectData.projectIdentifiers[platform]; - - await device.fileSystem.putFile(path.join(buildInfoFilePath, buildInfoFileName), deviceFilePath, appIdentifier); - } - - this.$logger.out(`Successfully installed on device with identifier '${device.deviceInfo.identifier}'.`); - } - - private async updateHashesOnDevice(data: { device: Mobile.IDevice, appIdentifier: string, outputFilePath: string, platformData: IPlatformData }): Promise { - const { device, appIdentifier, platformData, outputFilePath } = data; - - if (!this.$mobileHelper.isAndroidPlatform(platformData.normalizedPlatformName)) { - return; - } - - let hashes = {}; - const hashesFilePath = path.join(outputFilePath || platformData.getBuildOutputPath(null), constants.HASHES_FILE_NAME); - if (this.$fs.exists(hashesFilePath)) { - hashes = this.$fs.readJson(hashesFilePath); - } - - await device.fileSystem.updateHashesOnDevice(hashes, appIdentifier); - } - - public async deployPlatform(deployInfo: IDeployPlatformInfo): Promise { - await this.preparePlatform({ - platform: deployInfo.platform, - appFilesUpdaterOptions: deployInfo.appFilesUpdaterOptions, - projectData: deployInfo.projectData, - config: deployInfo.config, - nativePrepare: deployInfo.nativePrepare, - env: deployInfo.env - }); - const options: Mobile.IDevicesServicesInitializationOptions = { - platform: deployInfo.platform, deviceId: deployInfo.deployOptions.device, emulator: deployInfo.deployOptions.emulator - }; - await this.$devicesService.initialize(options); - const action = async (device: Mobile.IDevice): Promise => { - const buildConfig: IBuildConfig = { - buildForDevice: !this.$devicesService.isiOSSimulator(device), - iCloudContainerEnvironment: null, - projectDir: deployInfo.deployOptions.projectDir, - release: deployInfo.deployOptions.release, - device: deployInfo.deployOptions.device, - provision: deployInfo.deployOptions.provision, - teamId: deployInfo.deployOptions.teamId, - keyStoreAlias: deployInfo.deployOptions.keyStoreAlias, - keyStoreAliasPassword: deployInfo.deployOptions.keyStoreAliasPassword, - keyStorePassword: deployInfo.deployOptions.keyStorePassword, - keyStorePath: deployInfo.deployOptions.keyStorePath, - clean: deployInfo.deployOptions.clean - }; - - let installPackageFile: string; - const shouldBuild = await this.shouldBuild(deployInfo.platform, deployInfo.projectData, buildConfig, deployInfo.outputPath); - if (shouldBuild) { - installPackageFile = await deployInfo.buildPlatform(deployInfo.platform, buildConfig, deployInfo.projectData); - } else { - this.$logger.out("Skipping package build. No changes detected on the native side. This will be fast!"); - } - - if (deployInfo.deployOptions.forceInstall || shouldBuild || (await this.shouldInstall(device, deployInfo.projectData, buildConfig))) { - await this.installApplication(device, buildConfig, deployInfo.projectData, installPackageFile, deployInfo.outputPath); - } else { - this.$logger.out("Skipping install."); - } - - }; - - if (deployInfo.deployOptions.device) { - const device = await this.$devicesService.getDevice(deployInfo.deployOptions.device); - deployInfo.deployOptions.device = device.deviceInfo.identifier; - } - - await this.$devicesService.execute(action, this.getCanExecuteAction(deployInfo.platform, deployInfo.deployOptions)); - } - - public async startApplication(platform: string, runOptions: IRunPlatformOptions, appData: Mobile.IStartApplicationData): Promise { - this.$logger.out("Starting..."); - - const action = async (device: Mobile.IDevice) => { - await device.applicationManager.startApplication(appData); - this.$logger.out(`Successfully started on device with identifier '${device.deviceInfo.identifier}'.`); - }; - - await this.$devicesService.initialize({ platform: platform, deviceId: runOptions.device }); - - if (runOptions.device) { - const device = await this.$devicesService.getDevice(runOptions.device); - runOptions.device = device.deviceInfo.identifier; - } - - await this.$devicesService.execute(action, this.getCanExecuteAction(platform, runOptions)); - } - - private getBuildOutputPath(platform: string, platformData: IPlatformData, options: IBuildOutputOptions): string { - if (options.androidBundle) { - return platformData.bundleBuildOutputPath; - } - - if (platform.toLowerCase() === this.$devicePlatformsConstants.iOS.toLowerCase()) { - return platformData.getBuildOutputPath(options); - } - - return platformData.getBuildOutputPath(options); - } - - private async getDeviceBuildInfoFilePath(device: Mobile.IDevice, projectData: IProjectData): Promise { - const platform = device.deviceInfo.platform.toLowerCase(); - const deviceRootPath = await this.$devicePathProvider.getDeviceProjectRootPath(device, { - appIdentifier: projectData.projectIdentifiers[platform], - getDirname: true - }); - return helpers.fromWindowsRelativePathToUnix(path.join(deviceRootPath, buildInfoFileName)); - } - - private async getDeviceBuildInfo(device: Mobile.IDevice, projectData: IProjectData): Promise { - const deviceFilePath = await this.getDeviceBuildInfoFilePath(device, projectData); - try { - return JSON.parse(await this.readFile(device, deviceFilePath, projectData)); - } catch (e) { - return null; - } - } - - private getBuildInfo(platform: string, platformData: IPlatformData, options: IBuildOutputOptions, buildOutputPath?: string): IBuildInfo { - buildOutputPath = buildOutputPath || this.getBuildOutputPath(platform, platformData, options); - const buildInfoFile = path.join(buildOutputPath, buildInfoFileName); - if (this.$fs.exists(buildInfoFile)) { - try { - const buildInfoTime = this.$fs.readJson(buildInfoFile); - return buildInfoTime; - } catch (e) { - return null; - } - } - - return null; - } - - @helpers.hook('cleanApp') - public async cleanDestinationApp(platformInfo: IPreparePlatformInfo): Promise { - await this.ensurePlatformInstalled(platformInfo.platform, platformInfo.projectData, platformInfo.config, platformInfo.appFilesUpdaterOptions, platformInfo.nativePrepare); - - const platformData = this.$platformsData.getPlatformData(platformInfo.platform, platformInfo.projectData); - const appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); - const appUpdater = new AppFilesUpdater(platformInfo.projectData.appDirectoryPath, appDestinationDirectoryPath, platformInfo.appFilesUpdaterOptions, this.$fs); - appUpdater.cleanDestinationApp(); - } - - public lastOutputPath(platform: string, buildConfig: IBuildConfig, projectData: IProjectData, outputPath?: string): string { - let packageFile: string; - const platformData = this.$platformsData.getPlatformData(platform, projectData); - if (buildConfig.buildForDevice) { - packageFile = this.getLatestApplicationPackageForDevice(platformData, buildConfig, outputPath).packageName; - } else { - packageFile = this.getLatestApplicationPackageForEmulator(platformData, buildConfig, outputPath).packageName; - } - if (!packageFile || !this.$fs.exists(packageFile)) { - this.$errors.failWithoutHelp("Unable to find built application. Try 'tns build %s'.", platform); - } - return packageFile; - } - - public copyLastOutput(platform: string, targetPath: string, buildConfig: IBuildConfig, projectData: IProjectData): void { - platform = platform.toLowerCase(); - targetPath = path.resolve(targetPath); - - const packageFile = this.lastOutputPath(platform, buildConfig, projectData); - - this.$fs.ensureDirectoryExists(path.dirname(targetPath)); - - if (this.$fs.exists(targetPath) && this.$fs.getFsStats(targetPath).isDirectory()) { - const sourceFileName = path.basename(packageFile); - this.$logger.trace(`Specified target path: '${targetPath}' is directory. Same filename will be used: '${sourceFileName}'.`); - targetPath = path.join(targetPath, sourceFileName); - } - this.$fs.copyFile(packageFile, targetPath); - this.$logger.info(`Copied file '${packageFile}' to '${targetPath}'.`); - } - - public async removePlatforms(platforms: string[], projectData: IProjectData): Promise { - for (const platform of platforms) { - this.validatePlatformInstalled(platform, projectData); - const platformData = this.$platformsData.getPlatformData(platform, projectData); - let errorMessage; - - try { - await platformData.platformProjectService.stopServices(platformData.projectRoot); - } catch (err) { - errorMessage = err.message; - } - - try { - const platformDir = path.join(projectData.platformsDir, platform.toLowerCase()); - this.$fs.deleteDirectory(platformDir); - this.$projectDataService.removeNSProperty(projectData.projectDir, platformData.frameworkPackageName); - - this.$logger.out(`Platform ${platform} successfully removed.`); - } catch (err) { - this.$logger.error(`Failed to remove ${platform} platform with errors:`); - if (errorMessage) { - this.$logger.error(errorMessage); - } - this.$errors.failWithoutHelp(err.message); - } - } - } - - public async updatePlatforms(platforms: string[], projectData: IProjectData, config: IPlatformOptions): Promise { - for (const platformParam of platforms) { - const data = platformParam.split("@"), - platform = data[0], - version = data[1]; - - if (this.hasPlatformDirectory(platform, projectData)) { - await this.updatePlatform(platform, version, projectData, config); - } else { - await this.addPlatform(platformParam, projectData, config); - } - } - } - - private getCanExecuteAction(platform: string, options: IDeviceEmulator): any { - const canExecute = (currentDevice: Mobile.IDevice): boolean => { - if (options.device && currentDevice && currentDevice.deviceInfo) { - return currentDevice.deviceInfo.identifier === options.device; - } - - if (this.$mobileHelper.isiOSPlatform(platform) && this.$hostInfo.isDarwin) { - if (this.$devicesService.isOnlyiOSSimultorRunning() || options.emulator || this.$devicesService.isiOSSimulator(currentDevice)) { - return true; - } - - return this.$devicesService.isiOSDevice(currentDevice); - } - - return true; - }; - - return canExecute; - } - - public validatePlatform(platform: string, projectData: IProjectData): void { - if (!platform) { - this.$errors.fail("No platform specified."); - } - - platform = platform.split("@")[0].toLowerCase(); - - if (!this.$platformsData.getPlatformData(platform, projectData)) { - this.$errors.fail("Invalid platform %s. Valid platforms are %s.", platform, helpers.formatListOfNames(this.$platformsData.platformsNames)); - } - } - - public validatePlatformInstalled(platform: string, projectData: IProjectData): void { - this.validatePlatform(platform, projectData); - - if (!this.hasPlatformDirectory(platform, projectData)) { - this.$errors.fail("The platform %s is not added to this project. Please use 'tns platform add '", platform); - } - } - - public async ensurePlatformInstalled(platform: string, projectData: IProjectData, config: IPlatformOptions, appFilesUpdaterOptions: IAppFilesUpdaterOptions, nativePrepare?: INativePrepare): Promise { - let requiresNativePlatformAdd = false; - - const platformData = this.$platformsData.getPlatformData(platform, projectData); - const prepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData); - - // In case when no platform is added and webpack plugin is started it produces files in platforms folder. - // In this case {N} CLI needs to add platform and keeps the already produced files from webpack - const shouldPersistWebpackFiles = this.shouldPersistWebpackFiles(platform, projectData, prepareInfo, appFilesUpdaterOptions, nativePrepare); - if (shouldPersistWebpackFiles) { - await this.persistWebpackFiles(platform, projectData, config, platformData, nativePrepare); - return; - } - - const hasPlatformDirectory = this.hasPlatformDirectory(platform, projectData); - if (hasPlatformDirectory) { - const shouldAddNativePlatform = !nativePrepare || !nativePrepare.skipNativePrepare; - // In case there's no prepare info, it means only platform add had been executed. So we've come from CLI and we do not need to prepare natively. - requiresNativePlatformAdd = prepareInfo && prepareInfo.nativePlatformStatus === constants.NativePlatformStatus.requiresPlatformAdd; - if (requiresNativePlatformAdd && shouldAddNativePlatform) { - await this.addPlatform(platform, projectData, config, "", nativePrepare); - } - } else { - await this.addPlatform(platform, projectData, config, "", nativePrepare); - } - } - - private shouldPersistWebpackFiles(platform: string, projectData: IProjectData, prepareInfo: IPrepareInfo, appFilesUpdaterOptions: IAppFilesUpdaterOptions, nativePrepare: INativePrepare): boolean { - const hasPlatformDirectory = this.hasPlatformDirectory(platform, projectData); - const isWebpackWatcherStarted = this.$usbLiveSyncService.isInitialized; - const hasNativePlatformStatus = prepareInfo && prepareInfo.nativePlatformStatus; - const requiresPlatformAdd = prepareInfo && prepareInfo.nativePlatformStatus === constants.NativePlatformStatus.requiresPlatformAdd; - const shouldAddNativePlatform = !nativePrepare || !nativePrepare.skipNativePrepare; - const shouldAddPlatform = !hasNativePlatformStatus || (requiresPlatformAdd && shouldAddNativePlatform); - const result = appFilesUpdaterOptions.bundle && isWebpackWatcherStarted && hasPlatformDirectory && shouldAddPlatform; - return result; - } - - private async persistWebpackFiles(platform: string, projectData: IProjectData, config: IPlatformOptions, platformData: IPlatformData, nativePrepare?: INativePrepare): Promise { - const tmpDirectoryPath = path.join(projectData.projectDir, "platforms", `tmp-${platform}`); - this.$fs.deleteDirectory(tmpDirectoryPath); - this.$fs.ensureDirectoryExists(tmpDirectoryPath); - this.$fs.copyFile(path.join(platformData.appDestinationDirectoryPath, "*"), tmpDirectoryPath); - await this.addPlatform(platform, projectData, config, "", nativePrepare); - this.$fs.copyFile(path.join(tmpDirectoryPath, "*"), platformData.appDestinationDirectoryPath); - this.$fs.deleteDirectory(tmpDirectoryPath); - } - - private hasPlatformDirectory(platform: string, projectData: IProjectData): boolean { - return this.$fs.exists(path.join(projectData.platformsDir, platform.toLowerCase())); - } - - public isPlatformSupportedForOS(platform: string, projectData: IProjectData): boolean { - const targetedOS = this.$platformsData.getPlatformData(platform, projectData).targetedOS; - const res = !targetedOS || targetedOS.indexOf("*") >= 0 || targetedOS.indexOf(process.platform) >= 0; - return res; - } - - private isPlatformPrepared(platform: string, projectData: IProjectData): boolean { - const platformData = this.$platformsData.getPlatformData(platform, projectData); - return platformData.platformProjectService.isPlatformPrepared(platformData.projectRoot, projectData); - } - - private getApplicationPackages(buildOutputPath: string, validBuildOutputData: IValidBuildOutputData): IApplicationPackage[] { - // Get latest package` that is produced from build - let result = this.getApplicationPackagesCore(this.$fs.readDirectory(buildOutputPath).map(filename => path.join(buildOutputPath, filename)), validBuildOutputData.packageNames); - if (result) { - return result; - } - - const candidates = this.$fs.enumerateFilesInDirectorySync(buildOutputPath); - result = this.getApplicationPackagesCore(candidates, validBuildOutputData.packageNames); - if (result) { - return result; - } - - if (validBuildOutputData.regexes && validBuildOutputData.regexes.length) { - return this.createApplicationPackages(candidates.filter(filepath => _.some(validBuildOutputData.regexes, regex => regex.test(path.basename(filepath))))); - } - - return []; - } - - private getApplicationPackagesCore(candidates: string[], validPackageNames: string[]): IApplicationPackage[] { - const packages = candidates.filter(filePath => _.includes(validPackageNames, path.basename(filePath))); - if (packages.length > 0) { - return this.createApplicationPackages(packages); - } - - return null; - } - - private createApplicationPackages(packages: string[]): IApplicationPackage[] { - return packages.map(filepath => this.createApplicationPackage(filepath)); - } - - private createApplicationPackage(packageName: string): IApplicationPackage { - return { - packageName, - time: this.$fs.getFsStats(packageName).mtime - }; - } - - private getLatestApplicationPackage(buildOutputPath: string, validBuildOutputData: IValidBuildOutputData): IApplicationPackage { - let packages = this.getApplicationPackages(buildOutputPath, validBuildOutputData); - const packageExtName = path.extname(validBuildOutputData.packageNames[0]); - if (packages.length === 0) { - this.$errors.fail(`No ${packageExtName} found in ${buildOutputPath} directory.`); - } - - if (packages.length > 1) { - this.$logger.warn(`More than one ${packageExtName} found in ${buildOutputPath} directory. Using the last one produced from build.`); - } - - packages = _.sortBy(packages, pkg => pkg.time).reverse(); // We need to reverse because sortBy always sorts in ascending order - - return packages[0]; - } - - public getLatestApplicationPackageForDevice(platformData: IPlatformData, buildConfig: IBuildConfig, outputPath?: string): IApplicationPackage { - return this.getLatestApplicationPackage(outputPath || platformData.getBuildOutputPath(buildConfig), platformData.getValidBuildOutputData({ buildForDevice: true, release: buildConfig.release, androidBundle: buildConfig.androidBundle })); - } - - public getLatestApplicationPackageForEmulator(platformData: IPlatformData, buildConfig: IBuildConfig, outputPath?: string): IApplicationPackage { - outputPath = outputPath || this.getBuildOutputPath(platformData.normalizedPlatformName.toLowerCase(), platformData, buildConfig); - const buildOutputOptions: IBuildOutputOptions = { buildForDevice: false, release: buildConfig.release, androidBundle: buildConfig.androidBundle }; - return this.getLatestApplicationPackage(outputPath || platformData.getBuildOutputPath(buildConfig), platformData.getValidBuildOutputData(buildOutputOptions)); - } - - private async updatePlatform(platform: string, version: string, projectData: IProjectData, config: IPlatformOptions): Promise { - const platformData = this.$platformsData.getPlatformData(platform, projectData); - - const data = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName); - const currentVersion = data && data.version ? data.version : "0.2.0"; - - const installedModuleDir = temp.mkdirSync("runtime-to-update"); - let newVersion = version === constants.PackageVersion.NEXT ? - await this.$packageInstallationManager.getNextVersion(platformData.frameworkPackageName) : - version || await this.$packageInstallationManager.getLatestCompatibleVersion(platformData.frameworkPackageName); - await this.$pacoteService.extractPackage(`${platformData.frameworkPackageName}@${newVersion}`, installedModuleDir); - const cachedPackageData = this.$fs.readJson(path.join(installedModuleDir, "package.json")); - newVersion = (cachedPackageData && cachedPackageData.version) || newVersion; - - const canUpdate = platformData.platformProjectService.canUpdatePlatform(installedModuleDir, projectData); - if (canUpdate) { - if (!semver.valid(newVersion)) { - this.$errors.fail("The version %s is not valid. The version should consists from 3 parts separated by dot.", newVersion); - } - - if (!semver.gt(currentVersion, newVersion)) { - await this.updatePlatformCore(platformData, { currentVersion, newVersion, canUpdate }, projectData, config); - } else if (semver.eq(currentVersion, newVersion)) { - this.$errors.fail("Current and new version are the same."); - } else { - this.$errors.fail(`Your current version: ${currentVersion} is higher than the one you're trying to install ${newVersion}.`); - } - } else { - this.$errors.failWithoutHelp("Native Platform cannot be updated."); - } - } - - private async updatePlatformCore(platformData: IPlatformData, updateOptions: IUpdatePlatformOptions, projectData: IProjectData, config: IPlatformOptions): Promise { - let packageName = platformData.normalizedPlatformName.toLowerCase(); - await this.removePlatforms([packageName], projectData); - packageName = updateOptions.newVersion ? `${packageName}@${updateOptions.newVersion}` : packageName; - await this.addPlatform(packageName, projectData, config); - this.$logger.out("Successfully updated to version ", updateOptions.newVersion); - } - - // TODO: Remove this method from here. It has nothing to do with platform - public async readFile(device: Mobile.IDevice, deviceFilePath: string, projectData: IProjectData): Promise { - temp.track(); - const uniqueFilePath = temp.path({ suffix: ".tmp" }); - const platform = device.deviceInfo.platform.toLowerCase(); - try { - await device.fileSystem.getFile(deviceFilePath, projectData.projectIdentifiers[platform], uniqueFilePath); - } catch (e) { - return null; - } - - if (this.$fs.exists(uniqueFilePath)) { - const text = this.$fs.readText(uniqueFilePath); - shell.rm(uniqueFilePath); - return text; - } - - return null; - } - - private isPlatformAdded(platform: string, platformPath: string, projectData: IProjectData): boolean { - if (!this.$fs.exists(platformPath)) { - return false; - } - - const prepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData); - if (!prepareInfo) { - return true; - } - - return prepareInfo.nativePlatformStatus !== constants.NativePlatformStatus.requiresPlatformAdd; - } -} - -$injector.register("platformService", PlatformService); diff --git a/lib/services/platform/add-platform-service.ts b/lib/services/platform/add-platform-service.ts new file mode 100644 index 0000000000..115fe3d1b7 --- /dev/null +++ b/lib/services/platform/add-platform-service.ts @@ -0,0 +1,67 @@ +import * as path from "path"; +import * as temp from "temp"; +import { PROJECT_FRAMEWORK_FOLDER_NAME, NativePlatformStatus } from "../../constants"; +import { performanceLog } from "../../common/decorators"; + +export class AddPlatformService implements IAddPlatformService { + constructor( + private $fs: IFileSystem, + private $pacoteService: IPacoteService, + private $projectChangesService: IProjectChangesService, + private $projectDataService: IProjectDataService, + private $terminalSpinnerService: ITerminalSpinnerService + ) { } + + public async addPlatformSafe(projectData: IProjectData, platformData: IPlatformData, packageToInstall: string, nativePrepare: INativePrepare): Promise { + const spinner = this.$terminalSpinnerService.createSpinner(); + + try { + spinner.start(); + + const frameworkDirPath = await this.extractPackage(packageToInstall); + const frameworkPackageJsonContent = this.$fs.readJson(path.join(frameworkDirPath, "..", "package.json")); + const frameworkVersion = frameworkPackageJsonContent.version; + + await this.addJSPlatform(platformData, projectData, frameworkDirPath, frameworkVersion); + + if (!nativePrepare || !nativePrepare.skipNativePrepare) { + await this.addNativePlatform(platformData, projectData, frameworkDirPath, frameworkVersion); + } + + return frameworkVersion; + } catch (err) { + const platformPath = path.join(projectData.platformsDir, platformData.platformNameLowerCase); + this.$fs.deleteDirectory(platformPath); + throw err; + } finally { + spinner.stop(); + } + } + + private async extractPackage(pkg: string): Promise { + temp.track(); + const downloadedPackagePath = temp.mkdirSync("runtimeDir"); + await this.$pacoteService.extractPackage(pkg, downloadedPackagePath); + const frameworkDir = path.join(downloadedPackagePath, PROJECT_FRAMEWORK_FOLDER_NAME); + + return path.resolve(frameworkDir); + } + + private async addJSPlatform(platformData: IPlatformData, projectData: IProjectData, frameworkDirPath: string, frameworkVersion: string): Promise { + const frameworkPackageNameData = { version: frameworkVersion }; + this.$projectDataService.setNSValue(projectData.projectDir, platformData.frameworkPackageName, frameworkPackageNameData); + } + + @performanceLog() + private async addNativePlatform(platformData: IPlatformData, projectData: IProjectData, frameworkDirPath: string, frameworkVersion: string): Promise { + const platformDir = path.join(projectData.platformsDir, platformData.normalizedPlatformName.toLowerCase()); + this.$fs.deleteDirectory(platformDir); + + await platformData.platformProjectService.createProject(path.resolve(frameworkDirPath), frameworkVersion, projectData); + platformData.platformProjectService.ensureConfigurationFileInAppResources(projectData); + await platformData.platformProjectService.interpolateData(projectData); + platformData.platformProjectService.afterCreateProject(platformData.projectRoot, projectData); + this.$projectChangesService.setNativePlatformStatus(platformData, { nativePlatformStatus: NativePlatformStatus.requiresPrepare }); + } +} +$injector.register("addPlatformService", AddPlatformService); diff --git a/lib/services/platform/platform-validation-service.ts b/lib/services/platform/platform-validation-service.ts new file mode 100644 index 0000000000..6648803b51 --- /dev/null +++ b/lib/services/platform/platform-validation-service.ts @@ -0,0 +1,76 @@ +import * as helpers from "../../common/helpers"; +import * as path from "path"; + +export class PlatformValidationService implements IPlatformValidationService { + + constructor( + private $errors: IErrors, + private $fs: IFileSystem, + private $logger: ILogger, + private $mobileHelper: Mobile.IMobileHelper, + private $platformsDataService: IPlatformsDataService + ) { } + + public validatePlatform(platform: string, projectData: IProjectData): void { + if (!platform) { + this.$errors.fail("No platform specified."); + } + + platform = platform.split("@")[0].toLowerCase(); + + if (!this.$platformsDataService.getPlatformData(platform, projectData)) { + const platformNames = helpers.formatListOfNames(this.$mobileHelper.platformNames); + this.$errors.fail(`Invalid platform ${platform}. Valid platforms are ${platformNames}.`); + } + } + + public validatePlatformInstalled(platform: string, projectData: IProjectData): void { + this.validatePlatform(platform, projectData); + + const hasPlatformDirectory = this.$fs.exists(path.join(projectData.platformsDir, platform.toLowerCase())); + if (!hasPlatformDirectory) { + this.$errors.fail("The platform %s is not added to this project. Please use 'tns platform add '", platform); + } + } + + public async validateOptions(provision: true | string, teamId: true | string, projectData: IProjectData, platform?: string, aab?: boolean): Promise { + if (platform && !this.$mobileHelper.isAndroidPlatform(platform) && aab) { + this.$errors.failWithoutHelp("The --aab option is supported only for the Android platform."); + } + + if (platform) { + platform = this.$mobileHelper.normalizePlatformName(platform); + this.$logger.trace("Validate options for platform: " + platform); + const platformData = this.$platformsDataService.getPlatformData(platform, projectData); + + const result = await platformData.platformProjectService.validateOptions( + projectData.projectIdentifiers[platform.toLowerCase()], + provision, + teamId + ); + + return result; + } else { + let valid = true; + const platforms = this.$mobileHelper.platformNames.map(p => p.toLowerCase()); + for (const availablePlatform of platforms) { + this.$logger.trace("Validate options for platform: " + availablePlatform); + const platformData = this.$platformsDataService.getPlatformData(availablePlatform, projectData); + valid = valid && await platformData.platformProjectService.validateOptions( + projectData.projectIdentifiers[availablePlatform.toLowerCase()], + provision, + teamId + ); + } + + return valid; + } + } + + public isPlatformSupportedForOS(platform: string, projectData: IProjectData): boolean { + const targetedOS = this.$platformsDataService.getPlatformData(platform, projectData).targetedOS; + const res = !targetedOS || targetedOS.indexOf("*") >= 0 || targetedOS.indexOf(process.platform) >= 0; + return res; + } +} +$injector.register("platformValidationService", PlatformValidationService); diff --git a/lib/services/platform/prepare-native-platform-service.ts b/lib/services/platform/prepare-native-platform-service.ts new file mode 100644 index 0000000000..b017932d70 --- /dev/null +++ b/lib/services/platform/prepare-native-platform-service.ts @@ -0,0 +1,117 @@ + +import { hook } from "../../common/helpers"; +import { performanceLog } from "../../common/decorators"; +import * as path from "path"; +import { NativePlatformStatus, APP_FOLDER_NAME, APP_RESOURCES_FOLDER_NAME } from "../../constants"; + +export class PrepareNativePlatformService implements IPrepareNativePlatformService { + + constructor( + private $androidResourcesMigrationService: IAndroidResourcesMigrationService, + private $fs: IFileSystem, + public $hooksService: IHooksService, + private $nodeModulesBuilder: INodeModulesBuilder, + private $projectChangesService: IProjectChangesService, + ) { } + + @performanceLog() + @hook('prepareNativeApp') + public async prepareNativePlatform(platformData: IPlatformData, projectData: IProjectData, prepareData: IPrepareData): Promise { + const { nativePrepare, release } = prepareData; + if (nativePrepare && nativePrepare.skipNativePrepare) { + return false; + } + + const changesInfo = await this.$projectChangesService.checkForChanges(platformData, projectData, prepareData); + + const hasModulesChange = !changesInfo || changesInfo.modulesChanged; + const hasConfigChange = !changesInfo || changesInfo.configChanged; + const hasChangesRequirePrepare = !changesInfo || changesInfo.changesRequirePrepare; + + const hasChanges = hasModulesChange || hasConfigChange || hasChangesRequirePrepare; + + if (changesInfo.hasChanges) { + await this.cleanProject(platformData, projectData, { release }); + } + + // Move the native application resources from platforms/.../app/App_Resources + // to the right places in the native project, + // because webpack copies them on every build (not every change). + this.prepareAppResources(platformData, projectData); + + if (hasChangesRequirePrepare) { + await platformData.platformProjectService.prepareProject(projectData, prepareData); + } + + if (hasModulesChange) { + await this.$nodeModulesBuilder.prepareNodeModules(platformData, projectData); + } + + if (hasModulesChange || hasConfigChange) { + await platformData.platformProjectService.processConfigurationFilesFromAppResources(projectData, { release }); + await platformData.platformProjectService.handleNativeDependenciesChange(projectData, { release }); + } + + platformData.platformProjectService.interpolateConfigurationFile(projectData); + this.$projectChangesService.setNativePlatformStatus(platformData, { nativePlatformStatus: NativePlatformStatus.alreadyPrepared }); + + return hasChanges; + } + + private prepareAppResources(platformData: IPlatformData, projectData: IProjectData): void { + const appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); + const appResourcesDestinationDirectoryPath = path.join(appDestinationDirectoryPath, APP_RESOURCES_FOLDER_NAME); + + if (this.$fs.exists(appResourcesDestinationDirectoryPath)) { + platformData.platformProjectService.prepareAppResources(appResourcesDestinationDirectoryPath, projectData); + const appResourcesDestination = platformData.platformProjectService.getAppResourcesDestinationDirectoryPath(projectData); + this.$fs.ensureDirectoryExists(appResourcesDestination); + + if (platformData.normalizedPlatformName.toLowerCase() === "android") { + const appResourcesDirectoryPath = projectData.getAppResourcesDirectoryPath(); + const appResourcesDirStructureHasMigrated = this.$androidResourcesMigrationService.hasMigrated(appResourcesDirectoryPath); + const appResourcesAndroid = path.join(appResourcesDirectoryPath, platformData.normalizedPlatformName); + + if (appResourcesDirStructureHasMigrated) { + this.$fs.copyFile(path.join(appResourcesAndroid, "src", "*"), appResourcesDestination); + + this.$fs.deleteDirectory(appResourcesDestinationDirectoryPath); + return; + } + + // https://github.com/NativeScript/android-runtime/issues/899 + // App_Resources/Android/libs is reserved to user's aars and jars, but they should not be copied as resources + this.$fs.copyFile(path.join(appResourcesDestinationDirectoryPath, platformData.normalizedPlatformName, "*"), appResourcesDestination); + this.$fs.deleteDirectory(path.join(appResourcesDestination, "libs")); + + this.$fs.deleteDirectory(appResourcesDestinationDirectoryPath); + + return; + } + + this.$fs.copyFile(path.join(appResourcesDestinationDirectoryPath, platformData.normalizedPlatformName, "*"), appResourcesDestination); + + this.$fs.deleteDirectory(appResourcesDestinationDirectoryPath); + } + } + + private async cleanProject(platformData: IPlatformData, projectData: IProjectData, options: { release: boolean }): Promise { + // android build artifacts need to be cleaned up + // when switching between debug, release and webpack builds + if (platformData.platformNameLowerCase !== "android") { + return; + } + + const previousPrepareInfo = this.$projectChangesService.getPrepareInfo(platformData); + if (!previousPrepareInfo || previousPrepareInfo.nativePlatformStatus !== NativePlatformStatus.alreadyPrepared) { + return; + } + + const { release: previousWasRelease } = previousPrepareInfo; + const { release: currentIsRelease } = options; + if (previousWasRelease !== currentIsRelease) { + await platformData.platformProjectService.cleanProject(platformData.projectRoot, projectData); + } + } +} +$injector.register("prepareNativePlatformService", PrepareNativePlatformService); diff --git a/lib/services/platforms-data-service.ts b/lib/services/platforms-data-service.ts new file mode 100644 index 0000000000..6512d9daf0 --- /dev/null +++ b/lib/services/platforms-data-service.ts @@ -0,0 +1,23 @@ +export class PlatformsDataService implements IPlatformsDataService { + private platformsDataService: { [index: string]: any } = {}; + + constructor($androidProjectService: IPlatformProjectService, + $iOSProjectService: IPlatformProjectService) { + + this.platformsDataService = { + ios: $iOSProjectService, + android: $androidProjectService + }; + } + + public getPlatformData(platform: string, projectData: IProjectData): IPlatformData { + const platformKey = platform && _.first(platform.toLowerCase().split("@")); + let platformData: IPlatformData; + if (platformKey) { + platformData = this.platformsDataService[platformKey] && this.platformsDataService[platformKey].getPlatformData(projectData); + } + + return platformData; + } +} +$injector.register("platformsDataService", PlatformsDataService); diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index ef5881c741..b9ff50db08 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -9,8 +9,8 @@ export class PluginsService implements IPluginsService { private static NPM_CONFIG = { save: true }; - private get $platformsData(): IPlatformsData { - return this.$injector.resolve("platformsData"); + private get $platformsDataService(): IPlatformsDataService { + return this.$injector.resolve("platformsDataService"); } private get $projectDataService(): IProjectDataService { return this.$injector.resolve("projectDataService"); @@ -31,7 +31,8 @@ export class PluginsService implements IPluginsService { private $logger: ILogger, private $errors: IErrors, private $filesHashService: IFilesHashService, - private $injector: IInjector) { } + private $injector: IInjector, + private $mobileHelper: Mobile.IMobileHelper) { } public async add(plugin: string, projectData: IProjectData): Promise { await this.ensure(projectData); @@ -88,7 +89,7 @@ export class PluginsService implements IPluginsService { } public async preparePluginNativeCode(pluginData: IPluginData, platform: string, projectData: IProjectData): Promise { - const platformData = this.$platformsData.getPlatformData(platform, projectData); + const platformData = this.$platformsDataService.getPlatformData(platform, projectData); pluginData.pluginPlatformsFolderPath = (_platform: string) => path.join(pluginData.fullPath, "platforms", _platform.toLowerCase()); const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(platform); @@ -208,7 +209,7 @@ export class PluginsService implements IPluginsService { const data = cacheData.nativescript || cacheData.moduleInfo; if (pluginData.isPlugin) { - pluginData.platformsData = data.platforms; + pluginData.platformsDataService = data.platforms; pluginData.pluginVariables = data.variables; } @@ -242,11 +243,11 @@ export class PluginsService implements IPluginsService { } private async executeForAllInstalledPlatforms(action: (_pluginDestinationPath: string, pl: string, _platformData: IPlatformData) => Promise, projectData: IProjectData): Promise { - const availablePlatforms = _.keys(this.$platformsData.availablePlatforms); + const availablePlatforms = this.$mobileHelper.platformNames.map(p => p.toLowerCase()); for (const platform of availablePlatforms) { const isPlatformInstalled = this.$fs.exists(path.join(projectData.platformsDir, platform.toLowerCase())); if (isPlatformInstalled) { - const platformData = this.$platformsData.getPlatformData(platform.toLowerCase(), projectData); + const platformData = this.$platformsDataService.getPlatformData(platform.toLowerCase(), projectData); const pluginDestinationPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME, "tns_modules"); await action(pluginDestinationPath, platform.toLowerCase(), platformData); } @@ -254,7 +255,7 @@ export class PluginsService implements IPluginsService { } private getInstalledFrameworkVersion(platform: string, projectData: IProjectData): string { - const platformData = this.$platformsData.getPlatformData(platform, projectData); + const platformData = this.$platformsDataService.getPlatformData(platform, projectData); const frameworkData = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName); return frameworkData.version; } @@ -263,7 +264,7 @@ export class PluginsService implements IPluginsService { let isValid = true; const installedFrameworkVersion = this.getInstalledFrameworkVersion(platform, projectData); - const pluginPlatformsData = pluginData.platformsData; + const pluginPlatformsData = pluginData.platformsDataService; if (pluginPlatformsData) { const pluginVersion = (pluginPlatformsData)[platform]; if (!pluginVersion) { diff --git a/lib/services/prepare-data-service.ts b/lib/services/prepare-data-service.ts new file mode 100644 index 0000000000..b13a00d44f --- /dev/null +++ b/lib/services/prepare-data-service.ts @@ -0,0 +1,14 @@ +import { IOSPrepareData, AndroidPrepareData } from "../data/prepare-data"; + +export class PrepareDataService implements IPrepareDataService { + constructor(private $mobileHelper: Mobile.IMobileHelper) { } + + public getPrepareData(projectDir: string, platform: string, data: any) { + if (this.$mobileHelper.isiOSPlatform(platform)) { + return new IOSPrepareData(projectDir, platform, data); + } else if (this.$mobileHelper.isAndroidPlatform(platform)) { + return new AndroidPrepareData(projectDir, platform, data); + } + } +} +$injector.register("prepareDataService", PrepareDataService); diff --git a/lib/services/prepare-platform-js-service.ts b/lib/services/prepare-platform-js-service.ts deleted file mode 100644 index 0ea772d0bb..0000000000 --- a/lib/services/prepare-platform-js-service.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as temp from "temp"; -import { hook } from "../common/helpers"; -import { PreparePlatformService } from "./prepare-platform-service"; -import { performanceLog } from "./../common/decorators"; - -temp.track(); - -export class PreparePlatformJSService extends PreparePlatformService implements IPreparePlatformService { - - constructor($fs: IFileSystem, - $xmlValidator: IXmlValidator, - $hooksService: IHooksService, - private $projectDataService: IProjectDataService) { - super($fs, $hooksService, $xmlValidator); - } - - public async addPlatform(info: IAddPlatformInfo): Promise { - const frameworkPackageNameData: any = { version: info.installedVersion }; - this.$projectDataService.setNSValue(info.projectData.projectDir, info.platformData.frameworkPackageName, frameworkPackageNameData); - } - - @performanceLog() - @hook('prepareJSApp') - public async preparePlatform(config: IPreparePlatformJSInfo): Promise { - // intentionally left blank, keep the support for before-prepareJSApp and after-prepareJSApp hooks - } -} - -$injector.register("preparePlatformJSService", PreparePlatformJSService); diff --git a/lib/services/prepare-platform-native-service.ts b/lib/services/prepare-platform-native-service.ts deleted file mode 100644 index 5afbbb5dc6..0000000000 --- a/lib/services/prepare-platform-native-service.ts +++ /dev/null @@ -1,132 +0,0 @@ -import * as constants from "../constants"; -import * as path from "path"; -import { PreparePlatformService } from "./prepare-platform-service"; -import { performanceLog } from "../common/decorators"; - -export class PreparePlatformNativeService extends PreparePlatformService implements IPreparePlatformService { - - constructor($fs: IFileSystem, - $xmlValidator: IXmlValidator, - $hooksService: IHooksService, - private $nodeModulesBuilder: INodeModulesBuilder, - private $projectChangesService: IProjectChangesService, - private $androidResourcesMigrationService: IAndroidResourcesMigrationService) { - super($fs, $hooksService, $xmlValidator); - } - - @performanceLog() - public async addPlatform(info: IAddPlatformInfo): Promise { - await info.platformData.platformProjectService.createProject(path.resolve(info.frameworkDir), info.installedVersion, info.projectData, info.config); - info.platformData.platformProjectService.ensureConfigurationFileInAppResources(info.projectData); - await info.platformData.platformProjectService.interpolateData(info.projectData, info.config); - info.platformData.platformProjectService.afterCreateProject(info.platformData.projectRoot, info.projectData); - this.$projectChangesService.setNativePlatformStatus(info.platformData.normalizedPlatformName, info.projectData, - { nativePlatformStatus: constants.NativePlatformStatus.requiresPrepare }); - } - - @performanceLog() - public async preparePlatform(config: IPreparePlatformJSInfo): Promise { - if (config.changesInfo.hasChanges) { - await this.cleanProject(config.platform, config.appFilesUpdaterOptions, config.platformData, config.projectData); - } - - // Move the native application resources from platforms/.../app/App_Resources - // to the right places in the native project, - // because webpack copies them on every build (not every change). - if (!config.changesInfo || config.changesInfo.changesRequirePrepare || config.appFilesUpdaterOptions.bundle) { - this.prepareAppResources(config.platformData, config.projectData); - } - - if (!config.changesInfo || config.changesInfo.changesRequirePrepare) { - await config.platformData.platformProjectService.prepareProject(config.projectData, config.platformSpecificData); - } - - const hasModulesChange = !config.changesInfo || config.changesInfo.modulesChanged; - const hasConfigChange = !config.changesInfo || config.changesInfo.configChanged; - - if (hasModulesChange) { - const appDestinationDirectoryPath = path.join(config.platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); - const lastModifiedTime = this.$fs.exists(appDestinationDirectoryPath) ? this.$fs.getFsStats(appDestinationDirectoryPath).mtime : null; - - const tnsModulesDestinationPath = path.join(appDestinationDirectoryPath, constants.TNS_MODULES_FOLDER_NAME); - const nodeModulesData: INodeModulesData = { - absoluteOutputPath: tnsModulesDestinationPath, - appFilesUpdaterOptions: config.appFilesUpdaterOptions, - lastModifiedTime, - platform: config.platform, - projectData: config.projectData, - projectFilesConfig: config.projectFilesConfig - }; - - // Process node_modules folder - await this.$nodeModulesBuilder.prepareNodeModules({ nodeModulesData, release: config.appFilesUpdaterOptions.release }); - } - - if (hasModulesChange || hasConfigChange) { - await config.platformData.platformProjectService.processConfigurationFilesFromAppResources(config.projectData, { release: config.appFilesUpdaterOptions.release }); - await config.platformData.platformProjectService.handleNativeDependenciesChange(config.projectData, { release: config.appFilesUpdaterOptions.release }); - } - - config.platformData.platformProjectService.interpolateConfigurationFile(config.projectData, config.platformSpecificData); - this.$projectChangesService.setNativePlatformStatus(config.platform, config.projectData, - { nativePlatformStatus: constants.NativePlatformStatus.alreadyPrepared }); - } - - private prepareAppResources(platformData: IPlatformData, projectData: IProjectData): void { - const appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); - const appResourcesDestinationDirectoryPath = path.join(appDestinationDirectoryPath, constants.APP_RESOURCES_FOLDER_NAME); - - if (this.$fs.exists(appResourcesDestinationDirectoryPath)) { - platformData.platformProjectService.prepareAppResources(appResourcesDestinationDirectoryPath, projectData); - const appResourcesDestination = platformData.platformProjectService.getAppResourcesDestinationDirectoryPath(projectData); - this.$fs.ensureDirectoryExists(appResourcesDestination); - - if (platformData.normalizedPlatformName.toLowerCase() === "android") { - const appResourcesDirectoryPath = projectData.getAppResourcesDirectoryPath(); - const appResourcesDirStructureHasMigrated = this.$androidResourcesMigrationService.hasMigrated(appResourcesDirectoryPath); - const appResourcesAndroid = path.join(appResourcesDirectoryPath, platformData.normalizedPlatformName); - - if (appResourcesDirStructureHasMigrated) { - this.$fs.copyFile(path.join(appResourcesAndroid, "src", "*"), appResourcesDestination); - - this.$fs.deleteDirectory(appResourcesDestinationDirectoryPath); - return; - } - - // https://github.com/NativeScript/android-runtime/issues/899 - // App_Resources/Android/libs is reserved to user's aars and jars, but they should not be copied as resources - this.$fs.copyFile(path.join(appResourcesDestinationDirectoryPath, platformData.normalizedPlatformName, "*"), appResourcesDestination); - this.$fs.deleteDirectory(path.join(appResourcesDestination, "libs")); - - this.$fs.deleteDirectory(appResourcesDestinationDirectoryPath); - - return; - } - - this.$fs.copyFile(path.join(appResourcesDestinationDirectoryPath, platformData.normalizedPlatformName, "*"), appResourcesDestination); - - this.$fs.deleteDirectory(appResourcesDestinationDirectoryPath); - } - } - - private async cleanProject(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformData: IPlatformData, projectData: IProjectData): Promise { - // android build artifacts need to be cleaned up - // when switching between debug, release and webpack builds - if (platform.toLowerCase() !== "android") { - return; - } - - const previousPrepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData); - if (!previousPrepareInfo || previousPrepareInfo.nativePlatformStatus !== constants.NativePlatformStatus.alreadyPrepared) { - return; - } - - const { release: previousWasRelease, bundle: previousWasBundle } = previousPrepareInfo; - const { release: currentIsRelease, bundle: currentIsBundle } = appFilesUpdaterOptions; - if ((previousWasRelease !== currentIsRelease) || (previousWasBundle !== currentIsBundle)) { - await platformData.platformProjectService.cleanProject(platformData.projectRoot, projectData); - } - } -} - -$injector.register("preparePlatformNativeService", PreparePlatformNativeService); diff --git a/lib/services/prepare-platform-service.ts b/lib/services/prepare-platform-service.ts deleted file mode 100644 index f6553e37b4..0000000000 --- a/lib/services/prepare-platform-service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as constants from "../constants"; -import * as path from "path"; -import { AppFilesUpdater } from "./app-files-updater"; - -export class PreparePlatformService { - constructor(protected $fs: IFileSystem, - public $hooksService: IHooksService, - private $xmlValidator: IXmlValidator) { - } - - protected async copyAppFiles(copyAppFilesData: ICopyAppFilesData): Promise { - copyAppFilesData.platformData.platformProjectService.ensureConfigurationFileInAppResources(copyAppFilesData.projectData); - const appDestinationDirectoryPath = path.join(copyAppFilesData.platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); - - // Copy app folder to native project - this.$fs.ensureDirectoryExists(appDestinationDirectoryPath); - - const appUpdater = new AppFilesUpdater(copyAppFilesData.projectData.appDirectoryPath, appDestinationDirectoryPath, copyAppFilesData.appFilesUpdaterOptions, this.$fs); - const appUpdaterOptions: IUpdateAppOptions = { - beforeCopyAction: sourceFiles => { - this.$xmlValidator.validateXmlFiles(sourceFiles); - }, - filesToRemove: copyAppFilesData.filesToRemove - }; - // TODO: consider passing filesToSync in appUpdaterOptions - // this would currently lead to the following problem: imagine changing two files rapidly one after the other (transpilation for example) - // the first file would trigger the whole LiveSync process and the second will be queued - // after the first LiveSync is done the .nsprepare file is written and the second file is later on wrongly assumed as having been prepared - // because .nsprepare was written after both file changes - appUpdater.updateApp(appUpdaterOptions, copyAppFilesData.projectData); - } -} diff --git a/lib/services/project-changes-service.ts b/lib/services/project-changes-service.ts index 82d96bd384..70605294ab 100644 --- a/lib/services/project-changes-service.ts +++ b/lib/services/project-changes-service.ts @@ -1,24 +1,22 @@ import * as path from "path"; import { NODE_MODULES_FOLDER_NAME, NativePlatformStatus, PACKAGE_JSON_FILE_NAME, APP_GRADLE_FILE_NAME, BUILD_XCCONFIG_FILE_NAME } from "../constants"; import { getHash, hook } from "../common/helpers"; +import { PrepareData } from "../data/prepare-data"; const prepareInfoFileName = ".nsprepareinfo"; class ProjectChangesInfo implements IProjectChangesInfo { - public appFilesChanged: boolean; public appResourcesChanged: boolean; public modulesChanged: boolean; public configChanged: boolean; public packageChanged: boolean; public nativeChanged: boolean; - public bundleChanged: boolean; public signingChanged: boolean; public nativePlatformStatus: NativePlatformStatus; public get hasChanges(): boolean { return this.packageChanged || - this.appFilesChanged || this.appResourcesChanged || this.modulesChanged || this.configChanged || @@ -46,10 +44,8 @@ export class ProjectChangesService implements IProjectChangesService { private _outputProjectCTime: number; constructor( - private $platformsData: IPlatformsData, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $fs: IFileSystem, - private $filesHashService: IFilesHashService, private $logger: ILogger, public $hooksService: IHooksService) { } @@ -59,35 +55,31 @@ export class ProjectChangesService implements IProjectChangesService { } @hook("checkForChanges") - public async checkForChanges(checkForChangesOpts: ICheckForChangesOptions): Promise { - const { platform, projectData, projectChangesOptions } = checkForChangesOpts; - const platformData = this.$platformsData.getPlatformData(platform, projectData); + public async checkForChanges(platformData: IPlatformData, projectData: IProjectData, prepareData: PrepareData): Promise { this._changesInfo = new ProjectChangesInfo(); - const isNewPrepareInfo = await this.ensurePrepareInfo(platform, projectData, projectChangesOptions); + const isNewPrepareInfo = await this.ensurePrepareInfo(platformData, projectData, prepareData); if (!isNewPrepareInfo) { this._newFiles = 0; - this._changesInfo.appFilesChanged = await this.hasChangedAppFiles(projectData); - - this._changesInfo.packageChanged = this.isProjectFileChanged(projectData, platform); + this._changesInfo.packageChanged = this.isProjectFileChanged(projectData.projectDir, platformData); const platformResourcesDir = path.join(projectData.appResourcesDirectoryPath, platformData.normalizedPlatformName); this._changesInfo.appResourcesChanged = this.containsNewerFiles(platformResourcesDir, null, projectData); /*done because currently all node_modules are traversed, a possible improvement could be traversing only the production dependencies*/ - this._changesInfo.nativeChanged = projectChangesOptions.skipModulesNativeCheck ? false : this.containsNewerFiles( + this._changesInfo.nativeChanged = this.containsNewerFiles( path.join(projectData.projectDir, NODE_MODULES_FOLDER_NAME), path.join(projectData.projectDir, NODE_MODULES_FOLDER_NAME, "tns-ios-inspector"), projectData, this.fileChangeRequiresBuild); - this.$logger.trace(`Set nativeChanged to ${this._changesInfo.nativeChanged}. skipModulesNativeCheck is: ${projectChangesOptions.skipModulesNativeCheck}`); + this.$logger.trace(`Set nativeChanged to ${this._changesInfo.nativeChanged}.`); if (this._newFiles > 0 || this._changesInfo.nativeChanged) { this.$logger.trace(`Setting modulesChanged to true, newFiles: ${this._newFiles}, nativeChanged: ${this._changesInfo.nativeChanged}`); this._changesInfo.modulesChanged = true; } - if (platform === this.$devicePlatformsConstants.iOS.toLowerCase()) { + if (platformData.platformNameLowerCase === this.$devicePlatformsConstants.iOS.toLowerCase()) { this._changesInfo.configChanged = this.filesChanged([path.join(platformResourcesDir, platformData.configurationFileName), path.join(platformResourcesDir, "LaunchScreen.storyboard"), path.join(platformResourcesDir, BUILD_XCCONFIG_FILE_NAME) @@ -102,20 +94,16 @@ export class ProjectChangesService implements IProjectChangesService { this.$logger.trace(`Set value of configChanged to ${this._changesInfo.configChanged}`); } - if (checkForChangesOpts.projectChangesOptions.nativePlatformStatus !== NativePlatformStatus.requiresPlatformAdd) { - const projectService = platformData.platformProjectService; - await projectService.checkForChanges(this._changesInfo, projectChangesOptions, projectData); + if (!prepareData.nativePrepare || !prepareData.nativePrepare.skipNativePrepare) { + await platformData.platformProjectService.checkForChanges(this._changesInfo, prepareData, projectData); } - if (projectChangesOptions.bundle !== this._prepareInfo.bundle || projectChangesOptions.release !== this._prepareInfo.release) { - this.$logger.trace(`Setting all setting to true. Current options are: `, projectChangesOptions, " old prepare info is: ", this._prepareInfo); - this._changesInfo.appFilesChanged = true; + if (prepareData.release !== this._prepareInfo.release) { + this.$logger.trace(`Setting all setting to true. Current options are: `, prepareData, " old prepare info is: ", this._prepareInfo); this._changesInfo.appResourcesChanged = true; this._changesInfo.modulesChanged = true; - this._changesInfo.bundleChanged = true; this._changesInfo.configChanged = true; - this._prepareInfo.release = projectChangesOptions.release; - this._prepareInfo.bundle = projectChangesOptions.bundle; + this._prepareInfo.release = prepareData.release; } if (this._changesInfo.packageChanged) { this.$logger.trace("Set modulesChanged to true as packageChanged is true"); @@ -132,7 +120,7 @@ export class ProjectChangesService implements IProjectChangesService { this._prepareInfo.changesRequireBuildTime = this._prepareInfo.time; } - this._prepareInfo.projectFileHash = this.getProjectFileStrippedHash(projectData, platform); + this._prepareInfo.projectFileHash = this.getProjectFileStrippedHash(projectData.projectDir, platformData); } this._changesInfo.nativePlatformStatus = this._prepareInfo.nativePlatformStatus; @@ -141,14 +129,14 @@ export class ProjectChangesService implements IProjectChangesService { return this._changesInfo; } - public getPrepareInfoFilePath(platform: string, projectData: IProjectData): string { - const platformData = this.$platformsData.getPlatformData(platform, projectData); + public getPrepareInfoFilePath(platformData: IPlatformData): string { const prepareInfoFilePath = path.join(platformData.projectRoot, prepareInfoFileName); + return prepareInfoFilePath; } - public getPrepareInfo(platform: string, projectData: IProjectData): IPrepareInfo { - const prepareInfoFilePath = this.getPrepareInfoFilePath(platform, projectData); + public getPrepareInfo(platformData: IPlatformData): IPrepareInfo { + const prepareInfoFilePath = this.getPrepareInfoFilePath(platformData); let prepareInfo: IPrepareInfo = null; if (this.$fs.exists(prepareInfoFilePath)) { try { @@ -157,16 +145,17 @@ export class ProjectChangesService implements IProjectChangesService { prepareInfo = null; } } + return prepareInfo; } - public savePrepareInfo(platform: string, projectData: IProjectData): void { - const prepareInfoFilePath = this.getPrepareInfoFilePath(platform, projectData); + public savePrepareInfo(platformData: IPlatformData): void { + const prepareInfoFilePath = this.getPrepareInfoFilePath(platformData); this.$fs.writeJson(prepareInfoFilePath, this._prepareInfo); } - public setNativePlatformStatus(platform: string, projectData: IProjectData, addedPlatform: IAddedNativePlatform): void { - this._prepareInfo = this._prepareInfo || this.getPrepareInfo(platform, projectData); + public setNativePlatformStatus(platformData: IPlatformData, addedPlatform: IAddedNativePlatform): void { + this._prepareInfo = this._prepareInfo || this.getPrepareInfo(platformData); if (this._prepareInfo && addedPlatform.nativePlatformStatus === NativePlatformStatus.alreadyPrepared) { this._prepareInfo.nativePlatformStatus = addedPlatform.nativePlatformStatus; } else { @@ -175,52 +164,45 @@ export class ProjectChangesService implements IProjectChangesService { }; } - this.savePrepareInfo(platform, projectData); + this.savePrepareInfo(platformData); } - private async ensurePrepareInfo(platform: string, projectData: IProjectData, projectChangesOptions: IProjectChangesOptions): Promise { - this._prepareInfo = this.getPrepareInfo(platform, projectData); + private async ensurePrepareInfo(platformData: IPlatformData, projectData: IProjectData, prepareData: PrepareData): Promise { + this._prepareInfo = this.getPrepareInfo(platformData); if (this._prepareInfo) { - this._prepareInfo.nativePlatformStatus = this._prepareInfo.nativePlatformStatus && this._prepareInfo.nativePlatformStatus < projectChangesOptions.nativePlatformStatus ? - projectChangesOptions.nativePlatformStatus : - this._prepareInfo.nativePlatformStatus || projectChangesOptions.nativePlatformStatus; - - const platformData = this.$platformsData.getPlatformData(platform, projectData); const prepareInfoFile = path.join(platformData.projectRoot, prepareInfoFileName); this._outputProjectMtime = this.$fs.getFsStats(prepareInfoFile).mtime.getTime(); this._outputProjectCTime = this.$fs.getFsStats(prepareInfoFile).ctime.getTime(); return false; } + const nativePlatformStatus = (!prepareData.nativePrepare || !prepareData.nativePrepare.skipNativePrepare) ? + NativePlatformStatus.requiresPrepare : NativePlatformStatus.requiresPlatformAdd; this._prepareInfo = { time: "", - nativePlatformStatus: projectChangesOptions.nativePlatformStatus, - bundle: projectChangesOptions.bundle, - release: projectChangesOptions.release, + nativePlatformStatus, + release: prepareData.release, changesRequireBuild: true, - projectFileHash: this.getProjectFileStrippedHash(projectData, platform), - changesRequireBuildTime: null, - appFilesHashes: await this.$filesHashService.generateHashes(this.getAppFiles(projectData)) + projectFileHash: this.getProjectFileStrippedHash(projectData.projectDir, platformData), + changesRequireBuildTime: null }; this._outputProjectMtime = 0; this._outputProjectCTime = 0; this._changesInfo = this._changesInfo || new ProjectChangesInfo(); - this._changesInfo.appFilesChanged = true; this._changesInfo.appResourcesChanged = true; this._changesInfo.modulesChanged = true; this._changesInfo.configChanged = true; return true; } - private getProjectFileStrippedHash(projectData: IProjectData, platform: string): string { - platform = platform.toLowerCase(); - const projectFilePath = path.join(projectData.projectDir, PACKAGE_JSON_FILE_NAME); + private getProjectFileStrippedHash(projectDir: string, platformData: IPlatformData): string { + const projectFilePath = path.join(projectDir, PACKAGE_JSON_FILE_NAME); const projectFileContents = this.$fs.readJson(projectFilePath); _(this.$devicePlatformsConstants) .keys() .map(k => k.toLowerCase()) - .difference([platform]) + .difference([platformData.platformNameLowerCase]) .each(otherPlatform => { delete projectFileContents.nativescript[`tns-${otherPlatform}`]; }); @@ -228,9 +210,9 @@ export class ProjectChangesService implements IProjectChangesService { return getHash(JSON.stringify(projectFileContents)); } - private isProjectFileChanged(projectData: IProjectData, platform: string): boolean { - const projectFileStrippedContentsHash = this.getProjectFileStrippedHash(projectData, platform); - const prepareInfo = this.getPrepareInfo(platform, projectData); + private isProjectFileChanged(projectDir: string, platformData: IPlatformData): boolean { + const projectFileStrippedContentsHash = this.getProjectFileStrippedHash(projectDir, platformData); + const prepareInfo = this.getPrepareInfo(platformData); return projectFileStrippedContentsHash !== prepareInfo.projectFileHash; } @@ -335,20 +317,5 @@ export class ProjectChangesService implements IProjectChangesService { } return false; } - - private getAppFiles(projectData: IProjectData): string[] { - return this.$fs.enumerateFilesInDirectorySync(projectData.appDirectoryPath, (filePath: string, stat: IFsStats) => filePath !== projectData.appResourcesDirectoryPath); - } - - private async hasChangedAppFiles(projectData: IProjectData): Promise { - const files = this.getAppFiles(projectData); - const changedFiles = await this.$filesHashService.getChanges(files, this._prepareInfo.appFilesHashes || {}); - const hasChanges = changedFiles && _.keys(changedFiles).length > 0; - if (hasChanges) { - this._prepareInfo.appFilesHashes = await this.$filesHashService.generateHashes(files); - } - - return hasChanges; - } } $injector.register("projectChangesService", ProjectChangesService); diff --git a/lib/services/test-execution-service.ts b/lib/services/test-execution-service.ts index 2a7a9d7689..c4a91cb97e 100644 --- a/lib/services/test-execution-service.ts +++ b/lib/services/test-execution-service.ts @@ -1,6 +1,7 @@ import * as constants from "../constants"; import * as path from 'path'; import * as os from 'os'; +import { RunController } from "../controllers/run-controller"; interface IKarmaConfigOptions { debugBrk: boolean; @@ -11,118 +12,55 @@ export class TestExecutionService implements ITestExecutionService { private static CONFIG_FILE_NAME = `node_modules/${constants.TEST_RUNNER_NAME}/config.js`; private static SOCKETIO_JS_FILE_NAME = `node_modules/${constants.TEST_RUNNER_NAME}/socket.io.js`; - constructor(private $platformService: IPlatformService, - private $liveSyncService: ILiveSyncService, + constructor( + private $runController: RunController, private $httpClient: Server.IHttpClient, private $config: IConfiguration, private $logger: ILogger, private $fs: IFileSystem, private $options: IOptions, private $pluginsService: IPluginsService, - private $devicesService: Mobile.IDevicesService, - private $childProcess: IChildProcess) { - } + private $projectDataService: IProjectDataService, + private $childProcess: IChildProcess) { } public platform: string; - public async startKarmaServer(platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise { + public async startKarmaServer(platform: string, liveSyncInfo: ILiveSyncInfo, deviceDescriptors: ILiveSyncDeviceInfo[]): Promise { platform = platform.toLowerCase(); this.platform = platform; + const projectData = this.$projectDataService.getProjectData(liveSyncInfo.projectDir); + // We need the dependencies installed here, so we can start the Karma server. await this.$pluginsService.ensureAllDependenciesAreInstalled(projectData); - const projectDir = projectData.projectDir; - await this.$devicesService.initialize({ - platform: platform, - deviceId: this.$options.device, - emulator: this.$options.emulator - }); - - const karmaConfig = this.getKarmaConfiguration(platform, projectData), - // In case you want to debug the unit test runner, add "--inspect-brk=" as a first element in the array of args. - karmaRunner = this.$childProcess.spawn(process.execPath, [path.join(__dirname, "karma-execution.js")], { stdio: ["inherit", "inherit", "inherit", "ipc"] }), - launchKarmaTests = async (karmaData: any) => { + const karmaConfig = this.getKarmaConfiguration(platform, projectData); + // In case you want to debug the unit test runner, add "--inspect-brk=" as a first element in the array of args. + const karmaRunner = this.$childProcess.spawn(process.execPath, [path.join(__dirname, "karma-execution.js")], { stdio: ["inherit", "inherit", "inherit", "ipc"] }); + const launchKarmaTests = async (karmaData: any) => { this.$logger.trace("## Unit-testing: Parent process received message", karmaData); let port: string; if (karmaData.url) { port = karmaData.url.port; const socketIoJsUrl = `http://${karmaData.url.host}/socket.io/socket.io.js`; const socketIoJs = (await this.$httpClient.httpRequest(socketIoJsUrl)).body; - this.$fs.writeFile(path.join(projectDir, TestExecutionService.SOCKETIO_JS_FILE_NAME), socketIoJs); + this.$fs.writeFile(path.join(liveSyncInfo.projectDir, TestExecutionService.SOCKETIO_JS_FILE_NAME), socketIoJs); } if (karmaData.launcherConfig) { const configOptions: IKarmaConfigOptions = JSON.parse(karmaData.launcherConfig); const configJs = this.generateConfig(port, configOptions); - this.$fs.writeFile(path.join(projectDir, TestExecutionService.CONFIG_FILE_NAME), configJs); + this.$fs.writeFile(path.join(liveSyncInfo.projectDir, TestExecutionService.CONFIG_FILE_NAME), configJs); } // Prepare the project AFTER the TestExecutionService.CONFIG_FILE_NAME file is created in node_modules // so it will be sent to device. - let devices = []; - if (this.$options.debugBrk) { - const selectedDeviceForDebug = await this.$devicesService.pickSingleDevice({ - onlyEmulators: this.$options.emulator, - onlyDevices: this.$options.forDevice, - deviceId: this.$options.device - }); - devices = [selectedDeviceForDebug]; - // const debugData = this.getDebugData(platform, projectData, deployOptions, { device: selectedDeviceForDebug.deviceInfo.identifier }); - // await this.$debugService.debug(debugData, this.$options); - } else { - devices = this.$devicesService.getDeviceInstances(); - } - - // Now let's take data for each device: - const platformLowerCase = this.platform && this.platform.toLowerCase(); - const deviceDescriptors: ILiveSyncDeviceInfo[] = devices.filter(d => !platformLowerCase || d.deviceInfo.platform.toLowerCase() === platformLowerCase) - .map(d => { - const info: ILiveSyncDeviceInfo = { - identifier: d.deviceInfo.identifier, - buildAction: async (): Promise => { - const buildConfig: IBuildConfig = { - buildForDevice: !d.isEmulator, - iCloudContainerEnvironment: this.$options.iCloudContainerEnvironment, - projectDir: this.$options.path, - clean: this.$options.clean, - teamId: this.$options.teamId, - device: this.$options.device, - provision: this.$options.provision, - release: this.$options.release, - keyStoreAlias: this.$options.keyStoreAlias, - keyStorePath: this.$options.keyStorePath, - keyStoreAliasPassword: this.$options.keyStoreAliasPassword, - keyStorePassword: this.$options.keyStorePassword - }; - - await this.$platformService.buildPlatform(d.deviceInfo.platform, buildConfig, projectData); - const pathToBuildResult = await this.$platformService.lastOutputPath(d.deviceInfo.platform, buildConfig, projectData); - return pathToBuildResult; - }, - debugOptions: this.$options, - debugggingEnabled: this.$options.debugBrk - }; - - return info; - }); - - const env = this.$options.env || {}; - env.unitTesting = !!this.$options.bundle; - - const liveSyncInfo: ILiveSyncInfo = { - projectDir: projectData.projectDir, - skipWatcher: !this.$options.watch || this.$options.justlaunch, - watchAllFiles: this.$options.syncAllFiles, - bundle: !!this.$options.bundle, - release: this.$options.release, - env, - timeout: this.$options.timeout, - useHotModuleReload: this.$options.hmr - }; - - await this.$liveSyncService.liveSync(deviceDescriptors, liveSyncInfo); + await this.$runController.run({ + projectDir: liveSyncInfo.projectDir, + liveSyncInfo, + deviceDescriptors + }); }; karmaRunner.on("message", (karmaData: any) => { @@ -198,7 +136,7 @@ export class TestExecutionService implements ITestExecutionService { debugTransport: this.$options.debugTransport, debugBrk: this.$options.debugBrk, watch: !!this.$options.watch, - bundle: !!this.$options.bundle, + bundle: true, appDirectoryRelativePath: projectData.getAppDirectoryRelativePath() } }, @@ -218,7 +156,7 @@ export class TestExecutionService implements ITestExecutionService { } karmaConfig.projectDir = projectData.projectDir; - karmaConfig.bundle = this.$options.bundle; + karmaConfig.bundle = true; karmaConfig.platform = platform.toLowerCase(); this.$logger.debug(JSON.stringify(karmaConfig, null, 4)); diff --git a/lib/services/webpack/webpack-compiler-service.ts b/lib/services/webpack/webpack-compiler-service.ts new file mode 100644 index 0000000000..c657583381 --- /dev/null +++ b/lib/services/webpack/webpack-compiler-service.ts @@ -0,0 +1,210 @@ +import * as path from "path"; +import * as child_process from "child_process"; +import { EventEmitter } from "events"; +import { performanceLog } from "../../common/decorators"; +import { hook } from "../../common/helpers"; +import { WEBPACK_COMPILATION_COMPLETE } from "../../constants"; + +export class WebpackCompilerService extends EventEmitter implements IWebpackCompilerService { + private webpackProcesses: IDictionary = {}; + + constructor( + private $childProcess: IChildProcess, + public $hooksService: IHooksService, + private $logger: ILogger, + private $projectData: IProjectData, + ) { super(); } + + public async compileWithWatch(platformData: IPlatformData, projectData: IProjectData, config: IWebpackCompilerConfig): Promise { + return new Promise(async (resolve, reject) => { + if (this.webpackProcesses[platformData.platformNameLowerCase]) { + resolve(); + return; + } + + let isFirstWebpackWatchCompilation = true; + config.watch = true; + const childProcess = await this.startWebpackProcess(platformData, projectData, config); + + childProcess.on("message", (message: any) => { + if (message === "Webpack compilation complete.") { + this.$logger.info("Webpack build done!"); + resolve(childProcess); + } + + if (message.emittedFiles) { + if (isFirstWebpackWatchCompilation) { + isFirstWebpackWatchCompilation = false; + return; + } + + const result = this.getUpdatedEmittedFiles(message.emittedFiles); + + const files = result.emittedFiles + .filter((file: string) => file.indexOf("App_Resources") === -1) + .map((file: string) => path.join(platformData.appDestinationDirectoryPath, "app", file)); + + const data = { + files, + hmrData: { + hash: result.hash, + fallbackFiles: result.fallbackFiles + } + }; + + this.emit(WEBPACK_COMPILATION_COMPLETE, data); + } + }); + + childProcess.on("close", (arg: any) => { + const exitCode = typeof arg === "number" ? arg : arg && arg.code; + if (exitCode === 0) { + resolve(childProcess); + } else { + const error = new Error(`Executing webpack failed with exit code ${exitCode}.`); + error.code = exitCode; + reject(error); + } + }); + }); + } + + public async compileWithoutWatch(platformData: IPlatformData, projectData: IProjectData, config: IWebpackCompilerConfig): Promise { + return new Promise(async (resolve, reject) => { + if (this.webpackProcesses[platformData.platformNameLowerCase]) { + resolve(); + return; + } + + const childProcess = await this.startWebpackProcess(platformData, projectData, config); + childProcess.on("close", (arg: any) => { + const exitCode = typeof arg === "number" ? arg : arg && arg.code; + if (exitCode === 0) { + resolve(); + } else { + const error = new Error(`Executing webpack failed with exit code ${exitCode}.`); + error.code = exitCode; + reject(error); + } + }); + }); + } + + public stopWebpackCompiler(platform: string): void { + if (platform) { + this.stopWebpackForPlatform(platform); + } else { + Object.keys(this.webpackProcesses).forEach(pl => this.stopWebpackForPlatform(pl)); + } + } + + @performanceLog() + @hook('prepareJSApp') + private async startWebpackProcess(platformData: IPlatformData, projectData: IProjectData, config: IWebpackCompilerConfig): Promise { + const envData = this.buildEnvData(platformData.platformNameLowerCase, config.env); + const envParams = this.buildEnvCommandLineParams(envData, platformData); + + const args = [ + path.join(projectData.projectDir, "node_modules", "webpack", "bin", "webpack.js"), + "--preserve-symlinks", + `--config=${path.join(projectData.projectDir, "webpack.config.js")}`, + ...envParams + ]; + + if (config.watch) { + args.push("--watch"); + } + + const stdio = config.watch ? ["inherit", "inherit", "inherit", "ipc"] : "inherit"; + const childProcess = this.$childProcess.spawn("node", args, { cwd: projectData.projectDir, stdio }); + + this.webpackProcesses[platformData.platformNameLowerCase] = childProcess; + + return childProcess; + } + + private buildEnvData(platform: string, env: any) { + const envData = Object.assign({}, + env, + { [platform.toLowerCase()]: true } + ); + + const appPath = this.$projectData.getAppDirectoryRelativePath(); + const appResourcesPath = this.$projectData.getAppResourcesRelativeDirectoryPath(); + Object.assign(envData, + appPath && { appPath }, + appResourcesPath && { appResourcesPath } + ); + + return envData; + } + + private buildEnvCommandLineParams(envData: any, platformData: IPlatformData) { + const envFlagNames = Object.keys(envData); + // const snapshotEnvIndex = envFlagNames.indexOf("snapshot"); + // if (snapshotEnvIndex > -1 && !utils.shouldSnapshot(config)) { + // logSnapshotWarningMessage($logger); + // envFlagNames.splice(snapshotEnvIndex, 1); + // } + + const args: any[] = []; + envFlagNames.map(item => { + let envValue = envData[item]; + if (typeof envValue === "undefined") { + return; + } + if (typeof envValue === "boolean") { + if (envValue) { + args.push(`--env.${item}`); + } + } else { + if (!Array.isArray(envValue)) { + envValue = [envValue]; + } + + envValue.map((value: any) => args.push(`--env.${item}=${value}`)); + } + }); + + return args; + } + + private getUpdatedEmittedFiles(emittedFiles: string[]) { + let fallbackFiles: string[] = []; + let hotHash; + if (emittedFiles.some(x => x.endsWith('.hot-update.json'))) { + let result = emittedFiles.slice(); + const hotUpdateScripts = emittedFiles.filter(x => x.endsWith('.hot-update.js')); + hotUpdateScripts.forEach(hotUpdateScript => { + const { name, hash } = this.parseHotUpdateChunkName(hotUpdateScript); + hotHash = hash; + // remove bundle/vendor.js files if there's a bundle.XXX.hot-update.js or vendor.XXX.hot-update.js + result = result.filter(file => file !== `${name}.js`); + }); + //if applying of hot update fails, we must fallback to the full files + fallbackFiles = emittedFiles.filter(file => result.indexOf(file) === -1); + return { emittedFiles: result, fallbackFiles, hash: hotHash }; + } + + return { emittedFiles, fallbackFiles }; + } + + private parseHotUpdateChunkName(name: string) { + const matcher = /^(.+)\.(.+)\.hot-update/gm; + const matches = matcher.exec(name); + return { + name: matches[1] || "", + hash: matches[2] || "", + }; + } + + private stopWebpackForPlatform(platform: string) { + this.$logger.trace(`Stopping webpack watch for platform ${platform}.`); + const webpackProcess = this.webpackProcesses[platform]; + if (webpackProcess) { + webpackProcess.kill("SIGINT"); + delete this.webpackProcesses[platform]; + } + } +} +$injector.register("webpackCompilerService", WebpackCompilerService); diff --git a/lib/services/webpack/webpack.d.ts b/lib/services/webpack/webpack.d.ts new file mode 100644 index 0000000000..d58c7978ac --- /dev/null +++ b/lib/services/webpack/webpack.d.ts @@ -0,0 +1,157 @@ +import { EventEmitter } from "events"; +import { BuildData } from "../../data/build-data"; +import { PrepareData } from "../../data/prepare-data"; + +declare global { + interface IWebpackCompilerService extends EventEmitter { + compileWithWatch(platformData: IPlatformData, projectData: IProjectData, config: IWebpackCompilerConfig): Promise; + compileWithoutWatch(platformData: IPlatformData, projectData: IProjectData, config: IWebpackCompilerConfig): Promise; + stopWebpackCompiler(platform: string): void; + } + + interface IWebpackCompilerConfig { + env: IWebpackEnvOptions; + watch?: boolean; + } + + interface IWebpackEnvOptions { + sourceMap?: boolean; + uglify?: boolean; + } + + interface IProjectChangesService { + checkForChanges(platformData: IPlatformData, projectData: IProjectData, prepareData: IPrepareData): Promise; + getPrepareInfoFilePath(platformData: IPlatformData): string; + getPrepareInfo(platformData: IPlatformData): IPrepareInfo; + savePrepareInfo(platformData: IPlatformData): void; + setNativePlatformStatus(platformData: IPlatformData, addedPlatform: IAddedNativePlatform): void; + currentChanges: IProjectChangesInfo; + } + + interface IFilesChangeEventData { + platform: string; + files: string[]; + hmrData: IPlatformHmrData; + hasNativeChanges: boolean; + } + + interface IDeviceRestartApplicationService { + restartOnDevice(deviceDescriptor: ILiveSyncDeviceInfo, projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, platformLiveSyncService: IPlatformLiveSyncService): Promise; + } + + interface IPlatformProjectService extends NodeJS.EventEmitter, IPlatformProjectServiceBase { + getPlatformData(projectData: IProjectData): IPlatformData; + validate(projectData: IProjectData, options: IOptions, notConfiguredEnvOptions?: INotConfiguredEnvOptions): Promise; + createProject(frameworkDir: string, frameworkVersion: string, projectData: IProjectData): Promise; + interpolateData(projectData: IProjectData): Promise; + interpolateConfigurationFile(projectData: IProjectData): void; + + /** + * Executes additional actions after native project is created. + * @param {string} projectRoot Path to the real NativeScript project. + * @param {IProjectData} projectData DTO with information about the project. + * @returns {void} + */ + afterCreateProject(projectRoot: string, projectData: IProjectData): void; + + /** + * Gets first chance to validate the options provided as command line arguments. + * @param {string} projectId Project identifier - for example org.nativescript.test. + * @param {any} provision UUID of the provisioning profile used in iOS option validation. + * @returns {void} + */ + validateOptions(projectId?: string, provision?: true | string, teamId?: true | string): Promise; + + buildProject(projectRoot: string, projectData: IProjectData, buildConfig: T): Promise; + + /** + * Prepares images in Native project (for iOS). + * @param {IProjectData} projectData DTO with information about the project. + * @param {any} platformSpecificData Platform specific data required for project preparation. + * @returns {void} + */ + prepareProject(projectData: IProjectData, prepareData: T): Promise; + + /** + * Prepares App_Resources in the native project by clearing data from other platform and applying platform specific rules. + * @param {string} appResourcesDirectoryPath The place in the native project where the App_Resources are copied first. + * @param {IProjectData} projectData DTO with information about the project. + * @returns {void} + */ + prepareAppResources(appResourcesDirectoryPath: string, projectData: IProjectData): void; + + /** + * Defines if current platform is prepared (i.e. if /platforms/ dir exists). + * @param {string} projectRoot The project directory (path where root's package.json is located). + * @param {IProjectData} projectData DTO with information about the project. + * @returns {boolean} True in case platform is prepare (i.e. if /platforms/ dir exists), false otherwise. + */ + isPlatformPrepared(projectRoot: string, projectData: IProjectData): boolean; + + /** + * Checks if current platform can be updated to a newer versions. + * @param {string} newInstalledModuleDir Path to the native project. + * @param {IProjectData} projectData DTO with information about the project. + * @return {boolean} True if platform can be updated. false otherwise. + */ + canUpdatePlatform(newInstalledModuleDir: string, projectData: IProjectData): boolean; + + preparePluginNativeCode(pluginData: IPluginData, options?: any): Promise; + + /** + * Removes native code of a plugin (CocoaPods, jars, libs, src). + * @param {IPluginData} Plugins data describing the plugin which should be cleaned. + * @param {IProjectData} projectData DTO with information about the project. + * @returns {void} + */ + removePluginNativeCode(pluginData: IPluginData, projectData: IProjectData): Promise; + + beforePrepareAllPlugins(projectData: IProjectData, dependencies?: IDependencyData[]): Promise; + + handleNativeDependenciesChange(projectData: IProjectData, opts: IRelease): Promise; + + /** + * Gets the path wheren App_Resources should be copied. + * @returns {string} Path to native project, where App_Resources should be copied. + */ + getAppResourcesDestinationDirectoryPath(projectData: IProjectData): string; + + cleanDeviceTempFolder(deviceIdentifier: string, projectData: IProjectData): Promise; + processConfigurationFilesFromAppResources(projectData: IProjectData, opts: { release: boolean }): Promise; + + /** + * Ensures there is configuration file (AndroidManifest.xml, Info.plist) in app/App_Resources. + * @param {IProjectData} projectData DTO with information about the project. + * @returns {void} + */ + ensureConfigurationFileInAppResources(projectData: IProjectData): void; + + /** + * Stops all running processes that might hold a lock on the filesystem. + * Android: Gradle daemon processes are terminated. + * @param {IPlatformData} platformData The data for the specified platform. + * @returns {void} + */ + stopServices?(projectRoot: string): Promise; + + /** + * Removes build artifacts specific to the platform + * @param {string} projectRoot The root directory of the native project. + * @param {IProjectData} projectData DTO with information about the project. + * @returns {void} + */ + cleanProject?(projectRoot: string, projectData: IProjectData): Promise + + /** + * Check the current state of the project, and validate against the options. + * If there are parts in the project that are inconsistent with the desired options, marks them in the changeset flags. + */ + checkForChanges(changeset: IProjectChangesInfo, prepareData: T, projectData: IProjectData): Promise; + + /** + * Get the deployment target's version + * Currently implemented only for iOS -> returns the value of IPHONEOS_DEPLOYMENT_TARGET property from xcconfig file + */ + getDeploymentTarget?(projectData: IProjectData): any; + } +} \ No newline at end of file diff --git a/lib/services/workflow/workflow.d.ts b/lib/services/workflow/workflow.d.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/tools/node-modules/node-modules-builder.ts b/lib/tools/node-modules/node-modules-builder.ts index c9af8361fd..a42d8df47d 100644 --- a/lib/tools/node-modules/node-modules-builder.ts +++ b/lib/tools/node-modules/node-modules-builder.ts @@ -1,15 +1,27 @@ -import { NpmPluginPrepare } from "./node-modules-dest-copy"; - export class NodeModulesBuilder implements INodeModulesBuilder { constructor( - private $injector: IInjector, - private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder + private $logger: ILogger, + private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder, + private $pluginsService: IPluginsService ) { } - public async prepareNodeModules(opts: INodeModulesBuilderData): Promise { - const productionDependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies(opts.nodeModulesData.projectData.projectDir); - const npmPluginPrepare: NpmPluginPrepare = this.$injector.resolve(NpmPluginPrepare); - await npmPluginPrepare.preparePlugins(productionDependencies, opts.nodeModulesData.platform, opts.nodeModulesData.projectData); + public async prepareNodeModules(platformData: IPlatformData, projectData: IProjectData): Promise { + const dependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir); + if (_.isEmpty(dependencies)) { + return; + } + + await platformData.platformProjectService.beforePrepareAllPlugins(projectData, dependencies); + + for (const dependencyKey in dependencies) { + const dependency = dependencies[dependencyKey]; + const isPlugin = !!dependency.nativescript; + if (isPlugin) { + this.$logger.debug(`Successfully prepared plugin ${dependency.name} for ${platformData.normalizedPlatformName.toLowerCase()}.`); + const pluginData = this.$pluginsService.convertToPluginData(dependency, projectData.projectDir); + await this.$pluginsService.preparePluginNativeCode(pluginData, platformData.normalizedPlatformName.toLowerCase(), projectData); + } + } } } diff --git a/lib/tools/node-modules/node-modules-dest-copy.ts b/lib/tools/node-modules/node-modules-dest-copy.ts deleted file mode 100644 index 45af036501..0000000000 --- a/lib/tools/node-modules/node-modules-dest-copy.ts +++ /dev/null @@ -1,23 +0,0 @@ -export class NpmPluginPrepare { - constructor( - private $pluginsService: IPluginsService, - private $platformsData: IPlatformsData, - ) { } - - public async preparePlugins(dependencies: IDependencyData[], platform: string, projectData: IProjectData): Promise { - if (_.isEmpty(dependencies)) { - return; - } - - await this.$platformsData.getPlatformData(platform, projectData).platformProjectService.beforePrepareAllPlugins(projectData, dependencies); - - for (const dependencyKey in dependencies) { - const dependency = dependencies[dependencyKey]; - const isPlugin = !!dependency.nativescript; - if (isPlugin) { - const pluginData = this.$pluginsService.convertToPluginData(dependency, projectData.projectDir); - await this.$pluginsService.preparePluginNativeCode(pluginData, platform, projectData); - } - } - } -} diff --git a/package.json b/package.json index ad975a9576..b0f3dee87b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nativescript", "preferGlobal": true, - "version": "5.4.0", + "version": "6.0.0", "author": "Telerik ", "description": "Command-line interface for building NativeScript projects", "bin": { diff --git a/test/app-files-updates.ts b/test/app-files-updates.ts deleted file mode 100644 index 7ac8f94b39..0000000000 --- a/test/app-files-updates.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { assert } from "chai"; -import { AppFilesUpdater } from "../lib/services/app-files-updater"; -import * as yok from "../lib/common/yok"; - -require("should"); - -function createTestInjector(): IInjector { - const testInjector = new yok.Yok(); - - testInjector.register("projectData", { appResourcesDirectoryPath: "App_Resources" }); - - return testInjector; -} - -describe("App files cleanup", () => { - class CleanUpAppFilesUpdater extends AppFilesUpdater { - public deletedDestinationItems: string[] = []; - - constructor( - public destinationFiles: string[], - options: any - ) { - super("", "", options, null); - } - - public clean() { - this.cleanDestinationApp(); - } - - protected readDestinationDir(): string[] { - return this.destinationFiles; - } - - protected deleteDestinationItem(directoryItem: string): void { - this.deletedDestinationItems.push(directoryItem); - } - } - - _.each([true, false], bundle => { - it(`cleans up entire app when bundle is ${bundle}`, () => { - const updater = new CleanUpAppFilesUpdater([ - "file1", "dir1/file2", "App_Resources/Android/blah.png" - ], { bundle }); - updater.clean(); - assert.deepEqual(["file1", "dir1/file2", "App_Resources/Android/blah.png"], updater.deletedDestinationItems); - }); - }); -}); - -describe("App files copy", () => { - class CopyAppFilesUpdater extends AppFilesUpdater { - public copiedDestinationItems: string[] = []; - - constructor( - public sourceFiles: string[], - options: any - ) { - super("", "", options, null); - } - - protected readSourceDir(): string[] { - return this.sourceFiles; - } - - public copy(): void { - const injector = createTestInjector(); - const projectData = injector.resolve("projectData"); - this.copiedDestinationItems = this.resolveAppSourceFiles(projectData); - } - } - - it("copies all app files but app_resources when not bundling", () => { - const updater = new CopyAppFilesUpdater([ - "file1", "dir1/file2" - ], { bundle: false }); - updater.copy(); - assert.deepEqual(["file1", "dir1/file2"], updater.copiedDestinationItems); - }); - - it("skips copying files when bundling", () => { - const updater = new CopyAppFilesUpdater([ - "file1", "dir1/file2", "App_Resources/Android/blah.png" - ], { bundle: true }); - updater.copy(); - assert.deepEqual([], updater.copiedDestinationItems); - }); -}); diff --git a/test/controllers/add-platform-controller.ts b/test/controllers/add-platform-controller.ts new file mode 100644 index 0000000000..6ab114fa5a --- /dev/null +++ b/test/controllers/add-platform-controller.ts @@ -0,0 +1,115 @@ +import { InjectorStub, PacoteServiceStub } from "../stubs"; +import { PlatformController } from "../../lib/controllers/platform-controller"; +import { AddPlatformService } from "../../lib/services/platform/add-platform-service"; +import { assert } from "chai"; +import { format } from "util"; +import { AddPlaformErrors } from "../../lib/constants"; + +let actualMessage: string = null; +const latestFrameworkVersion = "5.3.1"; +let extractedPackageFromPacote: string = null; + +function createInjector(data?: { latestFrameworkVersion: string }) { + const version = (data && data.latestFrameworkVersion) || latestFrameworkVersion; + + const injector = new InjectorStub(); + injector.register("platformController", PlatformController); + injector.register("addPlatformService", AddPlatformService); + injector.register("pacoteService", PacoteServiceStub); + + injector.register("pacoteService", { + extractPackage: async (name: string): Promise => { extractedPackageFromPacote = name; } + }); + + const logger = injector.resolve("logger"); + logger.out = (message: string) => actualMessage = message; + + const packageInstallationManager = injector.resolve("packageInstallationManager"); + packageInstallationManager.getLatestCompatibleVersion = async () => version; + + const fs = injector.resolve("fs"); + fs.readJson = () => ({ version }); + + return injector; +} + +const projectDir = "/my/test/dir"; + +describe("PlatformController", () => { + const testCases = [ + { + name: "should add the platform (tns platform add @4.2.1)", + latestFrameworkVersion: "4.2.1" + }, + { + name: "should add the latest compatible version (tns platform add )", + latestFrameworkVersion, + getPlatformParam: (platform: string) => `${platform}@${latestFrameworkVersion}` + }, + { + name: "should add the platform when --frameworkPath is provided", + frameworkPath: "/my/path/to/framework.tgz", + latestFrameworkVersion: "5.4.0" + } + ]; + + afterEach(() => { + actualMessage = null; + }); + + _.each(testCases, testCase => { + _.each(["ios", "android"], platform => { + it(`${testCase.name} for ${platform} platform`, async () => { + const injector = createInjector({ latestFrameworkVersion: testCase.latestFrameworkVersion }); + + const platformParam = testCase.getPlatformParam ? testCase.getPlatformParam(platform) : platform; + const platformController: PlatformController = injector.resolve("platformController"); + await platformController.addPlatform({ projectDir, platform: platformParam, frameworkPath: testCase.frameworkPath }); + + const expectedMessage = `Platform ${platform} successfully added. v${testCase.latestFrameworkVersion}`; + assert.deepEqual(actualMessage, expectedMessage); + }); + }); + }); + + _.each(["ios", "android"], platform => { + it(`should fail when path passed frameworkPath does not exist for ${platform}`, async () => { + const frameworkPath = "invalidPath"; + const errorMessage = format(AddPlaformErrors.InvalidFrameworkPathStringFormat, frameworkPath); + + const injector = createInjector(); + const fs = injector.resolve("fs"); + fs.exists = (filePath: string) => filePath !== frameworkPath; + + const platformController: PlatformController = injector.resolve("platformController"); + + await assert.isRejected(platformController.addPlatform({ projectDir, platform, frameworkPath }), errorMessage); + }); + it(`should respect platform version in package.json's nativescript key for ${platform}`, async () => { + const version = "2.5.0"; + + const injector = createInjector(); + + const projectDataService = injector.resolve("projectDataService"); + projectDataService.getNSValue = () => ({ version }); + + const platformController: PlatformController = injector.resolve("platformController"); + await platformController.addPlatform({ projectDir, platform }); + + const expectedPackageToAdd = `tns-${platform}@${version}`; + assert.deepEqual(extractedPackageFromPacote, expectedPackageToAdd); + }); + it(`should install latest platform if no information found in package.json's nativescript key for ${platform}`, async () => { + const injector = createInjector(); + + const projectDataService = injector.resolve("projectDataService"); + projectDataService.getNSValue = () => null; + + const platformController: PlatformController = injector.resolve("platformController"); + await platformController.addPlatform({ projectDir, platform }); + + const expectedPackageToAdd = `tns-${platform}@${latestFrameworkVersion}`; + assert.deepEqual(extractedPackageFromPacote, expectedPackageToAdd); + }); + }); +}); diff --git a/test/controllers/prepare-controller.ts b/test/controllers/prepare-controller.ts new file mode 100644 index 0000000000..604a07c3cc --- /dev/null +++ b/test/controllers/prepare-controller.ts @@ -0,0 +1,121 @@ +import { assert } from "chai"; +import { PrepareController } from "../../lib/controllers/prepare-controller"; +import { InjectorStub } from "../stubs"; +import { PREPARE_READY_EVENT_NAME } from "../../lib/constants"; + +const projectDir = "/path/to/my/projecDir"; +const prepareData = { + projectDir, + release: false, + hmr: false, + env: {}, + watch: true +}; + +let isCompileWithWatchCalled = false; +let isCompileWithoutWatchCalled = false; +let isNativePrepareCalled = false; +let emittedEventNames: string[] = []; +let emittedEventData: any[] = []; + +function createTestInjector(data: { hasNativeChanges: boolean }): IInjector { + const injector = new InjectorStub(); + + injector.register("platformController", { + addPlatformIfNeeded: () => ({}) + }); + + injector.register("prepareNativePlatformService", ({ + prepareNativePlatform: async () => { + isNativePrepareCalled = true; + return data.hasNativeChanges; + } + })); + + injector.register("webpackCompilerService", ({ + on: () => ({}), + emit: () => ({}), + compileWithWatch: async () => { + isCompileWithWatchCalled = true; + }, + compileWithoutWatch: async () => { + isCompileWithoutWatchCalled = true; + } + })); + + injector.register("prepareController", PrepareController); + + const prepareController: PrepareController = injector.resolve("prepareController"); + prepareController.emit = (eventName: string, eventData: any) => { + emittedEventNames.push(eventName); + emittedEventData.push(eventData); + assert.isTrue(isCompileWithWatchCalled); + assert.isTrue(isNativePrepareCalled); + return true; + }; + + return injector; +} + +describe("prepareController", () => { + + afterEach(() => { + isNativePrepareCalled = false; + isCompileWithWatchCalled = false; + isCompileWithoutWatchCalled = false; + + emittedEventNames = []; + emittedEventData = []; + }); + + describe("preparePlatform with watch", () => { + _.each(["iOS", "Android"], platform => { + _.each([true, false], hasNativeChanges => { + it(`should execute native prepare and webpack's compilation for ${platform} platform when hasNativeChanges is ${hasNativeChanges}`, async () => { + const injector = createTestInjector({ hasNativeChanges }); + + const prepareController: PrepareController = injector.resolve("prepareController"); + await prepareController.prepare({ ...prepareData, platform }); + + assert.isTrue(isCompileWithWatchCalled); + assert.isTrue(isNativePrepareCalled); + }); + }); + it(`should respect native changes that are made before the initial preparation of the project had been done for ${platform}`, async () => { + const injector = createTestInjector({ hasNativeChanges: false }); + + const prepareController: PrepareController = injector.resolve("prepareController"); + + const prepareNativePlatformService = injector.resolve("prepareNativePlatformService"); + prepareNativePlatformService.prepareNativePlatform = async () => { + const nativeFilesWatcher = (prepareController).watchersData[projectDir][platform.toLowerCase()].nativeFilesWatcher; + nativeFilesWatcher.emit("all", "change", "my/project/App_Resources/some/file"); + isNativePrepareCalled = true; + return false; + }; + + await prepareController.prepare({ ...prepareData, platform }); + + assert.lengthOf(emittedEventNames, 1); + assert.lengthOf(emittedEventData, 1); + assert.deepEqual(emittedEventNames[0], PREPARE_READY_EVENT_NAME); + assert.deepEqual(emittedEventData[0], { files: [], hasNativeChanges: true, hmrData: null, platform: platform.toLowerCase() }); + }); + }); + }); + + describe("preparePlatform without watch", () => { + _.each(["ios", "android"], platform => { + it("shouldn't start the watcher when watch is false", async () => { + const injector = createTestInjector({ hasNativeChanges: false }); + + const prepareController: PrepareController = injector.resolve("prepareController"); + await prepareController.prepare({ ...prepareData, watch: false, platform }); + + assert.isTrue(isNativePrepareCalled); + assert.isTrue(isCompileWithoutWatchCalled); + assert.isFalse(isCompileWithWatchCalled); + }); + }); + }); +}); diff --git a/test/controllers/run-controller.ts b/test/controllers/run-controller.ts new file mode 100644 index 0000000000..6c4c4eeda0 --- /dev/null +++ b/test/controllers/run-controller.ts @@ -0,0 +1,275 @@ +import { RunController } from "../../lib/controllers/run-controller"; +import { InjectorStub } from "../stubs"; +import { LiveSyncServiceResolver } from "../../lib/resolvers/livesync-service-resolver"; +import { MobileHelper } from "../../lib/common/mobile/mobile-helper"; +import { assert } from "chai"; +import { RunEmitter } from "../../lib/emitters/run-emitter"; +import { RunOnDeviceEvents } from "../../lib/constants"; +import { PrepareData } from "../../lib/data/prepare-data"; +import { PrepareDataService } from "../../lib/services/prepare-data-service"; +import { BuildDataService } from "../../lib/services/build-data-service"; +import { PrepareController } from "../../lib/controllers/prepare-controller"; + +let isAttachToHmrStatusCalled = false; +let prepareData: IPrepareData = null; + +const appIdentifier = "org.nativescript.myCoolApp"; +const projectDir = "/path/to/my/projecDir"; +const buildOutputPath = `${projectDir}/platform/ios/build/myproject.app`; + +const iOSDevice = { deviceInfo: { identifier: "myiOSDevice", platform: "ios" } }; +const iOSDeviceDescriptor = { identifier: "myiOSDevice", buildAction: async () => buildOutputPath }; +const androidDevice = { deviceInfo: { identifier: "myAndroidDevice", platform: "android" } }; +const androidDeviceDescriptor = { identifier: "myAndroidDevice", buildAction: async () => buildOutputPath }; + +const map: IDictionary<{ device: Mobile.IDevice, descriptor: ILiveSyncDeviceInfo }> = { + myiOSDevice: { + device: iOSDevice, + descriptor: iOSDeviceDescriptor + }, + myAndroidDevice: { + device: androidDevice, + descriptor: androidDeviceDescriptor + } +}; + +const liveSyncInfo = { + projectDir, + release: false, + useHotModuleReload: false, + env: {} +}; + +function getFullSyncResult(): ILiveSyncResultInfo { + return { + modifiedFilesData: [], + isFullSync: true, + deviceAppData: { + appIdentifier + } + }; +} + +function mockDevicesService(injector: IInjector, devices: Mobile.IDevice[]) { + const devicesService: Mobile.IDevicesService = injector.resolve("devicesService"); + devicesService.execute = async (action: (device: Mobile.IDevice) => Promise, canExecute?: (dev: Mobile.IDevice) => boolean, options?: { allowNoDevices?: boolean }) => { + for (const d of devices) { + if (canExecute(d)) { + await action(d); + } + } + + return null; + }; +} + +function createTestInjector() { + const injector = new InjectorStub(); + + injector.register("addPlatformService", {}); + injector.register("buildArtefactsService", ({})); + injector.register("buildController", { + buildPlatform: async () => { + return buildOutputPath; + }, + buildPlatformIfNeeded: async () => ({}) + }); + injector.register("deviceInstallAppService", { + installOnDeviceIfNeeded: () => ({}) + }); + injector.register("deviceRefreshAppService", { + refreshApplication: () => ({}) + }); + injector.register("deviceDebugAppService", { + enableDebugging: () => ({}) + }); + injector.register("iOSLiveSyncService", { + fullSync: async () => getFullSyncResult(), + liveSyncWatchAction: () => ({}) + }); + injector.register("androidLiveSyncService", { + fullSync: async () => getFullSyncResult(), + liveSyncWatchAction: () => ({}) + }); + injector.register("hmrStatusService", { + attachToHmrStatusEvent: () => isAttachToHmrStatusCalled = true + }); + injector.register("liveSyncServiceResolver", LiveSyncServiceResolver); + injector.register("mobileHelper", MobileHelper); + injector.register("prepareController", { + stopWatchers: () => ({}), + prepare: async (currentPrepareData: PrepareData) => { + prepareData = currentPrepareData; + return { platform: prepareData.platform, hasNativeChanges: false }; + }, + on: () => ({}) + }); + injector.register("prepareNativePlatformService", {}); + injector.register("projectChangesService", {}); + injector.register("runController", RunController); + injector.register("runEmitter", RunEmitter); + injector.register("prepareDataService", PrepareDataService); + injector.register("buildDataService", BuildDataService); + injector.register("analyticsService", ({})); + + const devicesService = injector.resolve("devicesService"); + devicesService.getDevicesForPlatform = () => [{ identifier: "myTestDeviceId1" }]; + devicesService.getPlatformsFromDeviceDescriptors = (devices: ILiveSyncDeviceInfo[]) => devices.map(d => map[d.identifier].device.deviceInfo.platform); + devicesService.on = () => ({}); + + return injector; +} + +describe("RunController", () => { + let injector: IInjector = null; + let runController: RunController = null; + let runEmitter: RunEmitter = null; + + beforeEach(() => { + isAttachToHmrStatusCalled = false; + prepareData = null; + + injector = createTestInjector(); + runController = injector.resolve("runController"); + runEmitter = injector.resolve("runEmitter"); + }); + + describe("runOnDevices", () => { + describe("no watch", () => { + it("shouldn't start the watcher when skipWatcher flag is provided", async () => { + mockDevicesService(injector, [iOSDevice]); + + await runController.run({ + projectDir, + liveSyncInfo: { ...liveSyncInfo, skipWatcher: true }, + deviceDescriptors: [iOSDeviceDescriptor] + }); + + assert.isFalse(prepareData.watch); + }); + it("shouldn't attach to hmr status when skipWatcher flag is provided", async () => { + mockDevicesService(injector, [iOSDevice]); + + await runController.run({ + projectDir, + liveSyncInfo: { ...liveSyncInfo, skipWatcher: true, useHotModuleReload: true }, + deviceDescriptors: [iOSDeviceDescriptor] + }); + + assert.isFalse(isAttachToHmrStatusCalled); + }); + it("shouldn't attach to hmr status when useHotModuleReload is false", async () => { + mockDevicesService(injector, [iOSDevice]); + + await runController.run({ + projectDir, + liveSyncInfo, + deviceDescriptors: [iOSDeviceDescriptor] + }); + + assert.isFalse(isAttachToHmrStatusCalled); + }); + it("shouldn't attach to hmr status when no deviceDescriptors are provided", async () => { + mockDevicesService(injector, [iOSDevice]); + + await runController.run({ + projectDir, + liveSyncInfo, + deviceDescriptors: [] + }); + + assert.isFalse(isAttachToHmrStatusCalled); + }); + }); + describe("watch", () => { + const testCases = [ + { + name: "should prepare only ios platform when only ios devices are connected", + connectedDevices: [iOSDeviceDescriptor], + expectedPreparedPlatforms: ["ios"] + }, + { + name: "should prepare only android platform when only android devices are connected", + connectedDevices: [androidDeviceDescriptor], + expectedPreparedPlatforms: ["android"] + }, + { + name: "should prepare both platforms when ios and android devices are connected", + connectedDevices: [iOSDeviceDescriptor, androidDeviceDescriptor], + expectedPreparedPlatforms: ["ios", "android"] + } + ]; + + _.each(testCases, testCase => { + it(testCase.name, async () => { + mockDevicesService(injector, testCase.connectedDevices.map(d => map[d.identifier].device)); + + const preparedPlatforms: string[] = []; + const prepareController: PrepareController = injector.resolve("prepareController"); + prepareController.prepare = async (currentPrepareData: PrepareData) => { + preparedPlatforms.push(currentPrepareData.platform); + return { platform: currentPrepareData.platform, hasNativeChanges: false }; + }; + + await runController.run({ + projectDir, + liveSyncInfo, + deviceDescriptors: testCase.connectedDevices + }); + + assert.deepEqual(preparedPlatforms, testCase.expectedPreparedPlatforms); + }); + }); + }); + }); + + describe("stopRunOnDevices", () => { + const testCases = [ + { + name: "stops LiveSync operation for all devices and emits liveSyncStopped for all of them when stopLiveSync is called without deviceIdentifiers", + currentDeviceIdentifiers: ["device1", "device2", "device3"], + expectedDeviceIdentifiers: ["device1", "device2", "device3"] + }, + { + name: "stops LiveSync operation for all devices and emits liveSyncStopped for all of them when stopLiveSync is called without deviceIdentifiers (when a single device is attached)", + currentDeviceIdentifiers: ["device1"], + expectedDeviceIdentifiers: ["device1"] + }, + { + name: "stops LiveSync operation for specified devices and emits liveSyncStopped for each of them (when a single device is attached)", + currentDeviceIdentifiers: ["device1"], + expectedDeviceIdentifiers: ["device1"], + deviceIdentifiersToBeStopped: ["device1"] + }, + { + name: "stops LiveSync operation for specified devices and emits liveSyncStopped for each of them", + currentDeviceIdentifiers: ["device1", "device2", "device3"], + expectedDeviceIdentifiers: ["device1", "device3"], + deviceIdentifiersToBeStopped: ["device1", "device3"] + }, + { + name: "does not raise liveSyncStopped event for device, which is not currently being liveSynced", + currentDeviceIdentifiers: ["device1", "device2", "device3"], + expectedDeviceIdentifiers: ["device1"], + deviceIdentifiersToBeStopped: ["device1", "device4"] + } + ]; + + for (const testCase of testCases) { + it(testCase.name, async () => { + (runController).persistData(projectDir, testCase.currentDeviceIdentifiers.map(identifier => ({ identifier })), ["ios"]); + + const emittedDeviceIdentifiersForLiveSyncStoppedEvent: string[] = []; + + runEmitter.on(RunOnDeviceEvents.runOnDeviceStopped, (data: any) => { + assert.equal(data.projectDir, projectDir); + emittedDeviceIdentifiersForLiveSyncStoppedEvent.push(data.deviceIdentifier); + }); + + await runController.stop(projectDir, testCase.deviceIdentifiersToBeStopped); + + assert.deepEqual(emittedDeviceIdentifiersForLiveSyncStoppedEvent, testCase.expectedDeviceIdentifiers); + }); + } + }); +}); diff --git a/test/helpers/platform-command-helper.ts b/test/helpers/platform-command-helper.ts new file mode 100644 index 0000000000..0f71c8399b --- /dev/null +++ b/test/helpers/platform-command-helper.ts @@ -0,0 +1,96 @@ + +import { assert } from "chai"; +import { InjectorStub } from "../stubs"; +import { MobileHelper } from "../../lib/common/mobile/mobile-helper"; +import { DevicePlatformsConstants } from "../../lib/common/mobile/device-platforms-constants"; +import { PlatformCommandHelper } from "../../lib/helpers/platform-command-helper"; + +let isAddPlatformCalled = false; + +const projectDir = "/my/path/to/project"; +const projectData: any = { + projectDir, + platformsDir: "/my/path/to/project/platforms" +}; + +function createTestInjector() { + const injector = new InjectorStub(); + injector.register("addPlatformService", { + addPlatform: () => ({}) + }); + + injector.register("platformController", { + addPlatform: () => isAddPlatformCalled = true + }); + + injector.register("pacoteService", { + extractPackage: () => ({}) + }); + injector.register("platformValidationService", { + validatePlatform: () => ({}), + validatePlatformInstalled: () => ({}) + }); + + injector.register("platformCommandHelper", PlatformCommandHelper); + + injector.register("mobileHelper", MobileHelper); + injector.register("devicePlatformsConstants", DevicePlatformsConstants); + + return injector; +} + +describe("PlatformCommandHelper", () => { + let injector: IInjector = null; + let platformCommandHelper: IPlatformCommandHelper = null; + beforeEach(() => { + injector = createTestInjector(); + platformCommandHelper = injector.resolve("platformCommandHelper"); + }); + + describe("add platforms unit tests", () => { + _.each(["Android", "ANDROID", "android", "iOS", "IOS", "ios"], platform => { + beforeEach(() => { + isAddPlatformCalled = false; + }); + + it(`should not fail if platform is not normalized - ${platform}`, async () => { + const fs = injector.resolve("fs"); + fs.exists = () => false; + + await platformCommandHelper.addPlatforms([platform], projectData, null); + + assert.isTrue(isAddPlatformCalled); + }); + }); + _.each(["ios", "android"], platform => { + it(`should fail if ${platform} platform is already installed`, async () => { + (platformCommandHelper).isPlatformAdded = () => true; + + await assert.isRejected(platformCommandHelper.addPlatforms([platform], projectData, ""), `Platform ${platform} already added`); + }); + }); + }); + describe("clean platforms unit tests", () => { + _.each(["ios", "anroid"], platform => { + it(`should preserve the specified in the project nativescript version for ${platform}`, async () => { + let versionData = { version: "5.3.1" }; + + const projectDataService = injector.resolve("projectDataService"); + projectDataService.getNSValue = () => versionData; + projectDataService.removeNSProperty = () => { versionData = null; }; + + (platformCommandHelper).isPlatformAdded = () => false; + + await platformCommandHelper.cleanPlatforms([platform], injector.resolve("projectData"), ""); + }); + }); + }); + describe("update platforms unit tests", () => { + it("should fail when tha native platform cannot be updated", async () => { + const packageInstallationManager: IPackageInstallationManager = injector.resolve("packageInstallationManager"); + packageInstallationManager.getLatestVersion = async () => "0.2.0"; + + await assert.isRejected(platformCommandHelper.updatePlatforms(["android"], projectData), "Native Platform cannot be updated."); + }); + }); +}); diff --git a/test/ios-entitlements-service.ts b/test/ios-entitlements-service.ts index 95d7c17b6e..41e5101c14 100644 --- a/test/ios-entitlements-service.ts +++ b/test/ios-entitlements-service.ts @@ -17,7 +17,7 @@ describe("IOSEntitlements Service Tests", () => { const createTestInjector = (): IInjector => { const testInjector = new yok.Yok(); - testInjector.register('platformsData', stubs.PlatformsDataStub); + testInjector.register('platformsDataService', stubs.NativeProjectDataStub); testInjector.register('projectData', stubs.ProjectDataStub); testInjector.register("logger", stubs.LoggerStub); testInjector.register('iOSEntitlementsService', IOSEntitlementsService); diff --git a/test/ios-project-service.ts b/test/ios-project-service.ts index be0b2b1fb7..f443b533a6 100644 --- a/test/ios-project-service.ts +++ b/test/ios-project-service.ts @@ -28,7 +28,6 @@ import { NodePackageManager } from "../lib/node-package-manager"; import { YarnPackageManager } from "../lib/yarn-package-manager"; import { assert } from "chai"; -import { IOSProvisionService } from "../lib/services/ios-provision-service"; import { SettingsService } from "../lib/common/test/unit-tests/stubs"; import { BUILD_XCCONFIG_FILE_NAME } from "../lib/constants"; import { ProjectDataStub } from "./stubs"; @@ -711,161 +710,157 @@ describe("Relative paths", () => { }); }); -describe("iOS Project Service Signing", () => { - let testInjector: IInjector; - let projectName: string; - let projectDirName: string; - let projectPath: string; - let files: any; - let iOSProjectService: IPlatformProjectService; - let projectData: any; - let pbxproj: string; - let iOSProvisionService: IOSProvisionService; - let pbxprojDomXcode: IPbxprojDomXcode; - - beforeEach(() => { - files = {}; - projectName = "TNSApp" + Math.ceil(Math.random() * 1000); - projectDirName = projectName + "Dir"; - projectPath = temp.mkdirSync(projectDirName); - testInjector = createTestInjector(projectPath, projectDirName); - testInjector.register("fs", { - files: {}, - readJson(path: string): any { - if (this.exists(path)) { - return JSON.stringify(files[path]); - } else { - return null; - } - }, - exists(path: string): boolean { - return path in files; - } - }); - testInjector.register("pbxprojDomXcode", { Xcode: {} }); - pbxproj = join(projectPath, `platforms/ios/${projectDirName}.xcodeproj/project.pbxproj`); - iOSProjectService = testInjector.resolve("iOSProjectService"); - iOSProvisionService = testInjector.resolve("iOSProvisionService"); - pbxprojDomXcode = testInjector.resolve("pbxprojDomXcode"); - projectData = testInjector.resolve("projectData"); - iOSProvisionService.pick = async (uuidOrName: string, projId: string) => { - return ({ - "NativeScriptDev": { - Name: "NativeScriptDev", - CreationDate: null, - ExpirationDate: null, - TeamName: "Telerik AD", - TeamIdentifier: ["TKID101"], - ProvisionedDevices: [], - Entitlements: { - "application-identifier": "*", - "com.apple.developer.team-identifier": "ABC" - }, - UUID: "12345", - ProvisionsAllDevices: false, - ApplicationIdentifierPrefix: null, - DeveloperCertificates: null, - Type: "Development" - }, - "NativeScriptDist": { - Name: "NativeScriptDist", - CreationDate: null, - ExpirationDate: null, - TeamName: "Telerik AD", - TeamIdentifier: ["TKID202"], - ProvisionedDevices: [], - Entitlements: { - "application-identifier": "*", - "com.apple.developer.team-identifier": "ABC" - }, - UUID: "6789", - ProvisionsAllDevices: true, - ApplicationIdentifierPrefix: null, - DeveloperCertificates: null, - Type: "Distribution" - }, - "NativeScriptAdHoc": { - Name: "NativeScriptAdHoc", - CreationDate: null, - ExpirationDate: null, - TeamName: "Telerik AD", - TeamIdentifier: ["TKID303"], - ProvisionedDevices: [], - Entitlements: { - "application-identifier": "*", - "com.apple.developer.team-identifier": "ABC" - }, - UUID: "1010", - ProvisionsAllDevices: true, - ApplicationIdentifierPrefix: null, - DeveloperCertificates: null, - Type: "Distribution" - } - })[uuidOrName]; - }; - }); - - describe("Check for Changes", () => { - it("sets signingChanged if no Xcode project exists", async () => { - const changes = {}; - await iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev", teamId: undefined, useHotModuleReload: false }, projectData); - assert.isTrue(!!changes.signingChanged); - }); - it("sets signingChanged if the Xcode projects is configured with Automatic signing, but proivsion is specified", async () => { - files[pbxproj] = ""; - pbxprojDomXcode.Xcode.open = function (path: string) { - assert.equal(path, pbxproj); - return { - getSigning(x: string) { - return { style: "Automatic" }; - } - }; - }; - const changes = {}; - await iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev", teamId: undefined, useHotModuleReload: false }, projectData); - assert.isTrue(!!changes.signingChanged); - }); - it("sets signingChanged if the Xcode projects is configured with Manual signing, but the proivsion specified differs the selected in the pbxproj", async () => { - files[pbxproj] = ""; - pbxprojDomXcode.Xcode.open = function (path: string) { - assert.equal(path, pbxproj); - return { - getSigning() { - return { - style: "Manual", configurations: { - Debug: { name: "NativeScriptDev2" }, - Release: { name: "NativeScriptDev2" } - } - }; - } - }; - }; - const changes = {}; - await iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev", teamId: undefined, useHotModuleReload: false }, projectData); - assert.isTrue(!!changes.signingChanged); - }); - it("does not set signingChanged if the Xcode projects is configured with Manual signing and proivsion matches", async () => { - files[pbxproj] = ""; - pbxprojDomXcode.Xcode.open = function (path: string) { - assert.equal(path, pbxproj); - return { - getSigning() { - return { - style: "Manual", configurations: { - Debug: { name: "NativeScriptDev" }, - Release: { name: "NativeScriptDev" } - } - }; - } - }; - }; - const changes = {}; - await iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev", teamId: undefined, useHotModuleReload: false }, projectData); - console.log("CHANGES !!!! ", changes); - assert.isFalse(!!changes.signingChanged); - }); - }); -}); +// describe("iOS Project Service Signing", () => { +// let testInjector: IInjector; +// let projectName: string; +// let projectDirName: string; +// let projectPath: string; +// let files: any; +// let iOSProvisionService: IOSProvisionService; + +// // beforeEach(() => { +// // files = {}; +// // projectName = "TNSApp" + Math.ceil(Math.random() * 1000); +// // projectDirName = projectName + "Dir"; +// // projectPath = temp.mkdirSync(projectDirName); +// // testInjector = createTestInjector(projectPath, projectDirName); +// // testInjector.register("fs", { +// // files: {}, +// // readJson(path: string): any { +// // if (this.exists(path)) { +// // return JSON.stringify(files[path]); +// // } else { +// // return null; +// // } +// // }, +// // exists(path: string): boolean { +// // return path in files; +// // } +// // }); +// // testInjector.register("pbxprojDomXcode", { Xcode: {} }); +// // pbxproj = join(projectPath, `platforms/ios/${projectDirName}.xcodeproj/project.pbxproj`); +// // iOSProjectService = testInjector.resolve("iOSProjectService"); +// // iOSProvisionService = testInjector.resolve("iOSProvisionService"); +// // pbxprojDomXcode = testInjector.resolve("pbxprojDomXcode"); +// // projectData = testInjector.resolve("projectData"); +// // iOSProvisionService.pick = async (uuidOrName: string, projId: string) => { +// // return ({ +// // "NativeScriptDev": { +// // Name: "NativeScriptDev", +// // CreationDate: null, +// // ExpirationDate: null, +// // TeamName: "Telerik AD", +// // TeamIdentifier: ["TKID101"], +// // ProvisionedDevices: [], +// // Entitlements: { +// // "application-identifier": "*", +// // "com.apple.developer.team-identifier": "ABC" +// // }, +// // UUID: "12345", +// // ProvisionsAllDevices: false, +// // ApplicationIdentifierPrefix: null, +// // DeveloperCertificates: null, +// // Type: "Development" +// // }, +// // "NativeScriptDist": { +// // Name: "NativeScriptDist", +// // CreationDate: null, +// // ExpirationDate: null, +// // TeamName: "Telerik AD", +// // TeamIdentifier: ["TKID202"], +// // ProvisionedDevices: [], +// // Entitlements: { +// // "application-identifier": "*", +// // "com.apple.developer.team-identifier": "ABC" +// // }, +// // UUID: "6789", +// // ProvisionsAllDevices: true, +// // ApplicationIdentifierPrefix: null, +// // DeveloperCertificates: null, +// // Type: "Distribution" +// // }, +// // "NativeScriptAdHoc": { +// // Name: "NativeScriptAdHoc", +// // CreationDate: null, +// // ExpirationDate: null, +// // TeamName: "Telerik AD", +// // TeamIdentifier: ["TKID303"], +// // ProvisionedDevices: [], +// // Entitlements: { +// // "application-identifier": "*", +// // "com.apple.developer.team-identifier": "ABC" +// // }, +// // UUID: "1010", +// // ProvisionsAllDevices: true, +// // ApplicationIdentifierPrefix: null, +// // DeveloperCertificates: null, +// // Type: "Distribution" +// // } +// // })[uuidOrName]; +// // }; +// // }); + +// // describe("Check for Changes", () => { +// // it("sets signingChanged if no Xcode project exists", async () => { +// // const changes = {}; +// // await iOSProjectService.checkForChanges(changes, { release: false, provision: "NativeScriptDev", teamId: undefined, useHotModuleReload: false }, projectData); +// // assert.isTrue(!!changes.signingChanged); +// // }); +// // it("sets signingChanged if the Xcode projects is configured with Automatic signing, but proivsion is specified", async () => { +// // files[pbxproj] = ""; +// // pbxprojDomXcode.Xcode.open = function (path: string) { +// // assert.equal(path, pbxproj); +// // return { +// // getSigning(x: string) { +// // return { style: "Automatic" }; +// // } +// // }; +// // }; +// // const changes = {}; +// // await iOSProjectService.checkForChanges(changes, { release: false, provision: "NativeScriptDev", teamId: undefined, useHotModuleReload: false }, projectData); +// // assert.isTrue(!!changes.signingChanged); +// // }); +// // it("sets signingChanged if the Xcode projects is configured with Manual signing, but the proivsion specified differs the selected in the pbxproj", async () => { +// // files[pbxproj] = ""; +// // pbxprojDomXcode.Xcode.open = function (path: string) { +// // assert.equal(path, pbxproj); +// // return { +// // getSigning() { +// // return { +// // style: "Manual", configurations: { +// // Debug: { name: "NativeScriptDev2" }, +// // Release: { name: "NativeScriptDev2" } +// // } +// // }; +// // } +// // }; +// // }; +// // const changes = {}; +// // await iOSProjectService.checkForChanges(changes, { release: false, provision: "NativeScriptDev", teamId: undefined, useHotModuleReload: false }, projectData); +// // assert.isTrue(!!changes.signingChanged); +// // }); +// // it("does not set signingChanged if the Xcode projects is configured with Manual signing and proivsion matches", async () => { +// // files[pbxproj] = ""; +// // pbxprojDomXcode.Xcode.open = function (path: string) { +// // assert.equal(path, pbxproj); +// // return { +// // getSigning() { +// // return { +// // style: "Manual", configurations: { +// // Debug: { name: "NativeScriptDev" }, +// // Release: { name: "NativeScriptDev" } +// // } +// // }; +// // } +// // }; +// // }; +// // const changes = {}; +// // await iOSProjectService.checkForChanges(changes, { release: false, provision: "NativeScriptDev", teamId: undefined, useHotModuleReload: false }, projectData); +// // console.log("CHANGES !!!! ", changes); +// // assert.isFalse(!!changes.signingChanged); +// // }); +// // }); +// }); describe("Merge Project XCConfig files", () => { if (require("os").platform() !== "darwin") { diff --git a/test/nativescript-cli-lib.ts b/test/nativescript-cli-lib.ts index f32f35f779..f50dffe8f8 100644 --- a/test/nativescript-cli-lib.ts +++ b/test/nativescript-cli-lib.ts @@ -24,11 +24,11 @@ describe("nativescript-cli-lib", () => { "getAndroidAssetsStructure" ], constants: ["CONFIG_NS_APP_RESOURCES_ENTRY", "CONFIG_NS_APP_ENTRY", "CONFIG_NS_FILE_NAME"], - localBuildService: ["build"], + // localBuildService: ["build"], deviceLogProvider: null, packageManager: ["install", "uninstall", "view", "search"], extensibilityService: ["loadExtensions", "loadExtension", "getInstalledExtensions", "installExtension", "uninstallExtension"], - liveSyncService: ["liveSync", "stopLiveSync", "enableDebugging", "disableDebugging", "attachDebugger"], + // liveSyncService: ["liveSync", "stopLiveSync", "enableDebugging", "disableDebugging", "attachDebugger"], debugService: ["debug"], analyticsSettingsService: ["getClientId"], devicesService: [ diff --git a/test/platform-commands.ts b/test/platform-commands.ts index 5bba06ea46..0a3808baa5 100644 --- a/test/platform-commands.ts +++ b/test/platform-commands.ts @@ -4,7 +4,6 @@ import * as PlatformAddCommandLib from "../lib/commands/add-platform"; import * as PlatformRemoveCommandLib from "../lib/commands/remove-platform"; import * as PlatformUpdateCommandLib from "../lib/commands/update-platform"; import * as PlatformCleanCommandLib from "../lib/commands/platform-clean"; -import * as PlatformServiceLib from '../lib/services/platform-service'; import * as StaticConfigLib from "../lib/config"; import * as CommandsServiceLib from "../lib/common/services/commands-service"; import * as optionsLib from "../lib/options"; @@ -20,12 +19,15 @@ import * as ChildProcessLib from "../lib/common/child-process"; import ProjectChangesLib = require("../lib/services/project-changes-service"); import { Messages } from "../lib/common/messages/messages"; import { SettingsService } from "../lib/common/test/unit-tests/stubs"; +import { PlatformValidationService } from "../lib/services/platform/platform-validation-service"; +import { PlatformCommandHelper } from "../lib/helpers/platform-command-helper"; let isCommandExecuted = true; class PlatformData implements IPlatformData { frameworkPackageName = "tns-android"; normalizedPlatformName = "Android"; + platformNameLowerCase = "android"; platformProjectService: IPlatformProjectService = { validate: async (projectData: IProjectData): Promise => { return { @@ -78,10 +80,10 @@ class ErrorsNoFailStub implements IErrors { validateYargsArguments(parsed: any, knownOpts: any, shorthands: any, clientName?: string): void { /* intentionally left blank */ } } -class PlatformsData implements IPlatformsData { - platformsNames = ["android", "ios"]; +class PlatformsDataService implements IPlatformsDataService { + platformNames = ["android", "ios"]; getPlatformData(platform: string): IPlatformData { - if (_.includes(this.platformsNames, platform)) { + if (_.includes(this.platformNames, platform)) { return new PlatformData(); } @@ -100,12 +102,13 @@ function createTestInjector() { testInjector.register("hooksService", stubs.HooksServiceStub); testInjector.register("staticConfig", StaticConfigLib.StaticConfig); testInjector.register("nodeModulesDependenciesBuilder", {}); - testInjector.register('platformService', PlatformServiceLib.PlatformService); + testInjector.register('platformCommandHelper', PlatformCommandHelper); + testInjector.register('platformValidationService', PlatformValidationService); testInjector.register('errors', ErrorsNoFailStub); testInjector.register('logger', stubs.LoggerStub); testInjector.register('packageInstallationManager', stubs.PackageInstallationManagerStub); testInjector.register('projectData', stubs.ProjectDataStub); - testInjector.register('platformsData', PlatformsData); + testInjector.register('platformsDataService', PlatformsDataService); testInjector.register('devicesService', {}); testInjector.register('projectDataService', stubs.ProjectDataService); testInjector.register('prompter', {}); @@ -136,8 +139,7 @@ function createTestInjector() { testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); testInjector.register("xmlValidator", XmlValidator); testInjector.register("npm", {}); - testInjector.register("preparePlatformNativeService", {}); - testInjector.register("preparePlatformJSService", {}); + testInjector.register("prepareNativePlatformService", {}); testInjector.register("childProcess", ChildProcessLib.ChildProcess); testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService); testInjector.register("analyticsService", { @@ -182,19 +184,22 @@ function createTestInjector() { testInjector.register("cleanupService", { setShouldDispose: (shouldDispose: boolean): void => undefined }); + testInjector.register("addPlatformService", {}); + testInjector.register("platformController", {}); + testInjector.register("platformCommandHelper", PlatformCommandHelper); return testInjector; } describe('Platform Service Tests', () => { - let platformService: IPlatformService, testInjector: IInjector; + let platformCommandHelper: IPlatformCommandHelper, testInjector: IInjector; let commandsService: ICommandsService; let fs: IFileSystem; beforeEach(() => { testInjector = createTestInjector(); testInjector.register("fs", stubs.FileSystemStub); commandsService = testInjector.resolve("commands-service"); - platformService = testInjector.resolve("platformService"); + platformCommandHelper = testInjector.resolve("platformCommandHelper"); fs = testInjector.resolve("fs"); }); @@ -476,11 +481,11 @@ describe('Platform Service Tests', () => { const platformActions: { action: string, platforms: string[] }[] = []; const cleanCommand = testInjector.resolveCommand("platform|clean"); - platformService.removePlatforms = async (platforms: string[]) => { + platformCommandHelper.removePlatforms = async (platforms: string[]) => { platformActions.push({ action: "removePlatforms", platforms }); }; - platformService.addPlatforms = async (platforms: string[]) => { + platformCommandHelper.addPlatforms = async (platforms: string[]) => { platformActions.push({ action: "addPlatforms", platforms }); diff --git a/test/platform-service.ts b/test/platform-service.ts deleted file mode 100644 index 1cdac296f0..0000000000 --- a/test/platform-service.ts +++ /dev/null @@ -1,1153 +0,0 @@ -import * as yok from "../lib/common/yok"; -import * as stubs from "./stubs"; -import * as PlatformServiceLib from "../lib/services/platform-service"; -import * as StaticConfigLib from "../lib/config"; -import { VERSION_STRING, PACKAGE_JSON_FILE_NAME, AddPlaformErrors } from "../lib/constants"; -import * as fsLib from "../lib/common/file-system"; -import * as optionsLib from "../lib/options"; -import * as hostInfoLib from "../lib/common/host-info"; -import * as ProjectFilesManagerLib from "../lib/common/services/project-files-manager"; -import * as path from "path"; -import { format } from "util"; -import { assert } from "chai"; -import { LocalToDevicePathDataFactory } from "../lib/common/mobile/local-to-device-path-data-factory"; -import { MobileHelper } from "../lib/common/mobile/mobile-helper"; -import { ProjectFilesProvider } from "../lib/providers/project-files-provider"; -import { DevicePlatformsConstants } from "../lib/common/mobile/device-platforms-constants"; -import { XmlValidator } from "../lib/xml-validator"; -import { PreparePlatformNativeService } from "../lib/services/prepare-platform-native-service"; -import { PreparePlatformJSService } from "../lib/services/prepare-platform-js-service"; -import * as ChildProcessLib from "../lib/common/child-process"; -import ProjectChangesLib = require("../lib/services/project-changes-service"); -import { Messages } from "../lib/common/messages/messages"; -import { SettingsService } from "../lib/common/test/unit-tests/stubs"; -import { mkdir } from "shelljs"; -import * as constants from "../lib/constants"; - -require("should"); -const temp = require("temp"); -temp.track(); - -function createTestInjector() { - const testInjector = new yok.Yok(); - - testInjector.register('platformService', PlatformServiceLib.PlatformService); - testInjector.register('errors', stubs.ErrorsStub); - testInjector.register('logger', stubs.LoggerStub); - testInjector.register("nodeModulesDependenciesBuilder", {}); - testInjector.register('packageInstallationManager', stubs.PackageInstallationManagerStub); - // TODO: Remove the projectData - it shouldn't be required in the service itself. - testInjector.register('projectData', stubs.ProjectDataStub); - testInjector.register('platformsData', stubs.PlatformsDataStub); - testInjector.register('devicesService', {}); - testInjector.register('androidEmulatorServices', {}); - testInjector.register('projectDataService', stubs.ProjectDataService); - testInjector.register('prompter', {}); - testInjector.register('sysInfo', {}); - testInjector.register("commandsService", { - tryExecuteCommand: () => { /* intentionally left blank */ } - }); - testInjector.register("options", optionsLib.Options); - testInjector.register("hostInfo", hostInfoLib.HostInfo); - testInjector.register("staticConfig", StaticConfigLib.StaticConfig); - testInjector.register("nodeModulesBuilder", { - prepareNodeModules: () => { - return Promise.resolve(); - }, - prepareJSNodeModules: () => { - return Promise.resolve(); - } - }); - testInjector.register("pluginsService", { - getAllInstalledPlugins: () => { - return []; - }, - ensureAllDependenciesAreInstalled: () => { - return Promise.resolve(); - }, - validate: (platformData: IPlatformData, projectData: IProjectData) => { - return Promise.resolve(); - } - }); - testInjector.register("projectFilesManager", ProjectFilesManagerLib.ProjectFilesManager); - testInjector.register("hooksService", stubs.HooksServiceStub); - testInjector.register("localToDevicePathDataFactory", LocalToDevicePathDataFactory); - testInjector.register("mobileHelper", MobileHelper); - testInjector.register("projectFilesProvider", ProjectFilesProvider); - testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); - testInjector.register("xmlValidator", XmlValidator); - testInjector.register("preparePlatformNativeService", PreparePlatformNativeService); - testInjector.register("preparePlatformJSService", PreparePlatformJSService); - testInjector.register("packageManager", { - uninstall: async () => { - return true; - } - }); - testInjector.register("childProcess", ChildProcessLib.ChildProcess); - testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService); - testInjector.register("analyticsService", { - track: async (): Promise => undefined, - trackEventActionInGoogleAnalytics: () => Promise.resolve() - }); - testInjector.register("messages", Messages); - testInjector.register("devicePathProvider", {}); - testInjector.register("helpService", { - showCommandLineHelp: async (): Promise => (undefined) - }); - testInjector.register("settingsService", SettingsService); - testInjector.register("terminalSpinnerService", { - createSpinner: (msg: string) => ({ - start: (): void => undefined, - stop: (): void => undefined, - message: (): void => undefined - }) - }); - testInjector.register("androidResourcesMigrationService", stubs.AndroidResourcesMigrationServiceStub); - testInjector.register("filesHashService", { - generateHashes: () => Promise.resolve(), - getChanges: () => Promise.resolve({ test: "testHash" }) - }); - testInjector.register("pacoteService", { - extractPackage: async (packageName: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise => { - mkdir(path.join(destinationDirectory, "framework")); - (new fsLib.FileSystem(testInjector)).writeFile(path.join(destinationDirectory, PACKAGE_JSON_FILE_NAME), JSON.stringify({ - name: "package-name", - version: "1.0.0" - })); - } - }); - testInjector.register("usbLiveSyncService", () => ({})); - testInjector.register("doctorService", { - checkForDeprecatedShortImportsInAppDir: (projectDir: string): void => undefined - }); - testInjector.register("cleanupService", { - setShouldDispose: (shouldDispose: boolean): void => undefined - }); - - return testInjector; -} - -describe('Platform Service Tests', () => { - let platformService: IPlatformService, testInjector: IInjector; - const config: IPlatformOptions = { - ignoreScripts: false, - provision: null, - teamId: null, - sdk: null, - frameworkPath: null - }; - - beforeEach(() => { - testInjector = createTestInjector(); - testInjector.register("fs", stubs.FileSystemStub); - testInjector.resolve("projectData").initializeProjectData(); - platformService = testInjector.resolve("platformService"); - }); - - describe("add platform unit tests", () => { - describe("#add platform()", () => { - it("should not fail if platform is not normalized", async () => { - const fs = testInjector.resolve("fs"); - fs.exists = () => false; - const projectData: IProjectData = testInjector.resolve("projectData"); - await platformService.addPlatforms(["Android"], projectData, config); - await platformService.addPlatforms(["ANDROID"], projectData, config); - await platformService.addPlatforms(["AnDrOiD"], projectData, config); - await platformService.addPlatforms(["androiD"], projectData, config); - - await platformService.addPlatforms(["iOS"], projectData, config); - await platformService.addPlatforms(["IOS"], projectData, config); - await platformService.addPlatforms(["IoS"], projectData, config); - await platformService.addPlatforms(["iOs"], projectData, config); - }); - - it("should fail if platform is already installed", async () => { - const projectData: IProjectData = testInjector.resolve("projectData"); - // By default fs.exists returns true, so the platforms directory should exists - await assert.isRejected(platformService.addPlatforms(["android"], projectData, config), "Platform android already added"); - await assert.isRejected(platformService.addPlatforms(["ios"], projectData, config), "Platform ios already added"); - }); - - it("should fail if unable to extract runtime package", async () => { - const fs = testInjector.resolve("fs"); - fs.exists = () => false; - - const pacoteService = testInjector.resolve("pacoteService"); - const errorMessage = "Pacote service unable to extract package"; - pacoteService.extractPackage = async (packageName: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise => { - throw new Error(errorMessage); - }; - - const projectData: IProjectData = testInjector.resolve("projectData"); - await assert.isRejected(platformService.addPlatforms(["android"], projectData, config), errorMessage); - }); - - it("fails when path passed to frameworkPath does not exist", async () => { - const fs = testInjector.resolve("fs"); - fs.exists = () => false; - - const projectData: IProjectData = testInjector.resolve("projectData"); - const frameworkPath = "invalidPath"; - const errorMessage = format(AddPlaformErrors.InvalidFrameworkPathStringFormat, frameworkPath); - await assert.isRejected(platformService.addPlatforms(["android"], projectData, config, frameworkPath), errorMessage); - }); - - const assertCorrectDataIsPassedToPacoteService = async (versionString: string): Promise => { - const fs = testInjector.resolve("fs"); - fs.exists = () => false; - - const pacoteService = testInjector.resolve("pacoteService"); - let packageNamePassedToPacoteService = ""; - pacoteService.extractPackage = async (name: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise => { - packageNamePassedToPacoteService = name; - }; - - const platformsData = testInjector.resolve("platformsData"); - const packageName = "packageName"; - platformsData.getPlatformData = (platform: string, pData: IProjectData): IPlatformData => { - return { - frameworkPackageName: packageName, - platformProjectService: new stubs.PlatformProjectServiceStub(), - projectRoot: "", - normalizedPlatformName: "", - appDestinationDirectoryPath: "", - getBuildOutputPath: () => "", - getValidBuildOutputData: (buildOptions: IBuildOutputOptions) => ({ packageNames: [] }), - frameworkFilesExtensions: [], - relativeToFrameworkConfigurationFilePath: "", - fastLivesyncFileExtensions: [] - }; - }; - const projectData: IProjectData = testInjector.resolve("projectData"); - - await platformService.addPlatforms(["android"], projectData, config); - assert.equal(packageNamePassedToPacoteService, `${packageName}@${versionString}`); - await platformService.addPlatforms(["ios"], projectData, config); - assert.equal(packageNamePassedToPacoteService, `${packageName}@${versionString}`); - }; - it("should respect platform version in package.json's nativescript key", async () => { - const versionString = "2.5.0"; - const nsValueObject: any = { - [VERSION_STRING]: versionString - }; - const projectDataService = testInjector.resolve("projectDataService"); - projectDataService.getNSValue = () => nsValueObject; - - await assertCorrectDataIsPassedToPacoteService(versionString); - }); - - it("should install latest platform if no information found in package.json's nativescript key", async () => { - - const projectDataService = testInjector.resolve("projectDataService"); - projectDataService.getNSValue = (): any => null; - - const latestCompatibleVersion = "1.0.0"; - const packageInstallationManager = testInjector.resolve("packageInstallationManager"); - packageInstallationManager.getLatestCompatibleVersion = async (packageName: string, referenceVersion?: string): Promise => { - return latestCompatibleVersion; - }; - - await assertCorrectDataIsPassedToPacoteService(latestCompatibleVersion); - }); - - // Workflow: tns preview; tns platform add - it(`should add platform when only .js part of the platform has already been added (nativePlatformStatus is ${constants.NativePlatformStatus.requiresPlatformAdd})`, async () => { - const fs = testInjector.resolve("fs"); - fs.exists = () => true; - const projectChangesService = testInjector.resolve("projectChangesService"); - projectChangesService.getPrepareInfo = () => ({ nativePlatformStatus: constants.NativePlatformStatus.requiresPlatformAdd }); - const projectData = testInjector.resolve("projectData"); - let isJsPlatformAdded = false; - const preparePlatformJSService = testInjector.resolve("preparePlatformJSService"); - preparePlatformJSService.addPlatform = async () => isJsPlatformAdded = true; - let isNativePlatformAdded = false; - const preparePlatformNativeService = testInjector.resolve("preparePlatformNativeService"); - preparePlatformNativeService.addPlatform = async () => isNativePlatformAdded = true; - - await platformService.addPlatforms(["android"], projectData, config); - - assert.isTrue(isJsPlatformAdded); - assert.isTrue(isNativePlatformAdded); - }); - - // Workflow: tns platform add; tns platform add - it("shouldn't add platform when platforms folder exist and no .nsprepare file", async () => { - const fs = testInjector.resolve("fs"); - fs.exists = () => true; - const projectChangesService = testInjector.resolve("projectChangesService"); - projectChangesService.getPrepareInfo = () => null; - const projectData = testInjector.resolve("projectData"); - - await assert.isRejected(platformService.addPlatforms(["android"], projectData, config), "Platform android already added"); - }); - - // Workflow: tns run; tns platform add - it(`shouldn't add platform when both native and .js parts of the platform have already been added (nativePlatformStatus is ${constants.NativePlatformStatus.alreadyPrepared})`, async () => { - const fs = testInjector.resolve("fs"); - fs.exists = () => true; - const projectChangesService = testInjector.resolve("projectChangesService"); - projectChangesService.getPrepareInfo = () => ({ nativePlatformStatus: constants.NativePlatformStatus.alreadyPrepared }); - const projectData = testInjector.resolve("projectData"); - - await assert.isRejected(platformService.addPlatforms(["android"], projectData, config), "Platform android already added"); - }); - }); - }); - - describe("remove platform unit tests", () => { - it("should fail when platforms are not added", async () => { - const ExpectedErrorsCaught = 2; - let errorsCaught = 0; - const projectData: IProjectData = testInjector.resolve("projectData"); - testInjector.resolve("fs").exists = () => false; - - try { - await platformService.removePlatforms(["android"], projectData); - } catch (e) { - errorsCaught++; - } - - try { - await platformService.removePlatforms(["ios"], projectData); - } catch (e) { - errorsCaught++; - } - - assert.isTrue(errorsCaught === ExpectedErrorsCaught); - }); - it("shouldn't fail when platforms are added", async () => { - const projectData: IProjectData = testInjector.resolve("projectData"); - testInjector.resolve("fs").exists = () => false; - await platformService.addPlatforms(["android"], projectData, config); - - testInjector.resolve("fs").exists = () => true; - await platformService.removePlatforms(["android"], projectData); - }); - }); - - describe("clean platform unit tests", () => { - it("should preserve the specified in the project nativescript version", async () => { - const versionString = "2.4.1"; - const fs = testInjector.resolve("fs"); - fs.exists = () => false; - - const nsValueObject: any = {}; - nsValueObject[VERSION_STRING] = versionString; - const projectDataService = testInjector.resolve("projectDataService"); - projectDataService.getNSValue = () => nsValueObject; - - const packageInstallationManager = testInjector.resolve("packageInstallationManager"); - packageInstallationManager.install = (packageName: string, packageDir: string, options: INpmInstallOptions) => { - assert.deepEqual(options.version, versionString); - return ""; - }; - - const projectData: IProjectData = testInjector.resolve("projectData"); - platformService.removePlatforms = (platforms: string[], prjctData: IProjectData): Promise => { - nsValueObject[VERSION_STRING] = undefined; - return Promise.resolve(); - }; - - await platformService.cleanPlatforms(["android"], projectData, config); - - nsValueObject[VERSION_STRING] = versionString; - await platformService.cleanPlatforms(["ios"], projectData, config); - }); - }); - - // TODO: Commented as it doesn't seem correct. Check what's the case and why it's been expected to fail. - // describe("list platform unit tests", () => { - // it("fails when platforms are not added", () => { - // assert.throws(async () => await platformService.getAvailablePlatforms()); - // }); - // }); - - describe("update Platform", () => { - describe("#updatePlatform(platform)", () => { - it("should fail when the versions are the same", async () => { - const packageInstallationManager: IPackageInstallationManager = testInjector.resolve("packageInstallationManager"); - packageInstallationManager.getLatestVersion = async () => "0.2.0"; - const projectData: IProjectData = testInjector.resolve("projectData"); - - await assert.isRejected(platformService.updatePlatforms(["android"], projectData, null)); - }); - }); - }); - - // TODO: check this tests with QAs - // describe("prepare platform unit tests", () => { - // let fs: IFileSystem; - - // beforeEach(() => { - // testInjector = createTestInjector(); - // testInjector.register("fs", fsLib.FileSystem); - // fs = testInjector.resolve("fs"); - // testInjector.resolve("projectData").initializeProjectData(); - // }); - - // function prepareDirStructure() { - // const tempFolder = temp.mkdirSync("prepare_platform"); - - // const appFolderPath = path.join(tempFolder, "app"); - // fs.createDirectory(appFolderPath); - - // const nodeModulesPath = path.join(tempFolder, "node_modules"); - // fs.createDirectory(nodeModulesPath); - - // const testsFolderPath = path.join(appFolderPath, "tests"); - // fs.createDirectory(testsFolderPath); - - // const app1FolderPath = path.join(tempFolder, "app1"); - // fs.createDirectory(app1FolderPath); - - // const appDestFolderPath = path.join(tempFolder, "appDest"); - // const appResourcesFolderPath = path.join(appDestFolderPath, "App_Resources"); - // const appResourcesPath = path.join(appFolderPath, "App_Resources/Android"); - // fs.createDirectory(appResourcesPath); - // fs.writeFile(path.join(appResourcesPath, "test.txt"), "test"); - // fs.writeJson(path.join(tempFolder, "package.json"), { - // name: "testname", - // nativescript: { - // id: "org.nativescript.testname" - // } - // }); - - // return { tempFolder, appFolderPath, app1FolderPath, appDestFolderPath, appResourcesFolderPath }; - // } - - // async function execPreparePlatform(platformToTest: string, testDirData: any, - // release?: boolean) { - // const platformsData = testInjector.resolve("platformsData"); - // platformsData.platformsNames = ["ios", "android"]; - // platformsData.getPlatformData = (platform: string) => { - // return { - // appDestinationDirectoryPath: testDirData.appDestFolderPath, - // appResourcesDestinationDirectoryPath: testDirData.appResourcesFolderPath, - // normalizedPlatformName: platformToTest, - // configurationFileName: platformToTest === "ios" ? INFO_PLIST_FILE_NAME : MANIFEST_FILE_NAME, - // projectRoot: testDirData.tempFolder, - // platformProjectService: { - // prepareProject: (): any => null, - // validate: () => Promise.resolve(), - // createProject: (projectRoot: string, frameworkDir: string) => Promise.resolve(), - // interpolateData: (projectRoot: string) => Promise.resolve(), - // afterCreateProject: (projectRoot: string): any => null, - // getAppResourcesDestinationDirectoryPath: (pData: IProjectData, frameworkVersion?: string): string => { - // if (platform.toLowerCase() === "ios") { - // const dirPath = path.join(testDirData.appDestFolderPath, "Resources"); - // fs.ensureDirectoryExists(dirPath); - // return dirPath; - // } else { - // const dirPath = path.join(testDirData.appDestFolderPath, "src", "main", "res"); - // fs.ensureDirectoryExists(dirPath); - // return dirPath; - // } - // }, - // processConfigurationFilesFromAppResources: () => Promise.resolve(), - // handleNativeDependenciesChange: () => Promise.resolve(), - // ensureConfigurationFileInAppResources: (): any => null, - // interpolateConfigurationFile: (): void => undefined, - // isPlatformPrepared: (projectRoot: string) => false, - // prepareAppResources: (appResourcesDirectoryPath: string, pData: IProjectData): void => undefined, - // checkForChanges: () => { /* */ } - // } - // }; - // }; - - // const projectData = testInjector.resolve("projectData"); - // projectData.projectDir = testDirData.tempFolder; - // projectData.projectName = "app"; - // projectData.appDirectoryPath = testDirData.appFolderPath; - // projectData.appResourcesDirectoryPath = path.join(testDirData.appFolderPath, "App_Resources"); - - // platformService = testInjector.resolve("platformService"); - // const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: false, release: release, useHotModuleReload: false }; - // await platformService.preparePlatform({ - // platform: platformToTest, - // appFilesUpdaterOptions, - // platformTemplate: "", - // projectData, - // config: { provision: null, teamId: null, sdk: null, frameworkPath: null, ignoreScripts: false }, - // env: {} - // }); - // } - - // async function testPreparePlatform(platformToTest: string, release?: boolean): Promise { - // const testDirData = prepareDirStructure(); - // const created: CreatedTestData = new CreatedTestData(); - // created.testDirData = testDirData; - - // // Add platform specific files to app and app1 folders - // const platformSpecificFiles = [ - // "test1.ios.js", "test1-ios-js", "test2.android.js", "test2-android-js", - // "main.js" - // ]; - - // const destinationDirectories = [testDirData.appFolderPath, testDirData.app1FolderPath]; - - // _.each(destinationDirectories, directoryPath => { - // _.each(platformSpecificFiles, filePath => { - // const fileFullPath = path.join(directoryPath, filePath); - // fs.writeFile(fileFullPath, "testData"); - - // created.files.push(fileFullPath); - // }); - // }); - - // // Add App_Resources file to app and app1 folders - // _.each(destinationDirectories, directoryPath => { - // const iosIconFullPath = path.join(directoryPath, "App_Resources/iOS/icon.png"); - // fs.writeFile(iosIconFullPath, "test-image"); - // created.resources.ios.push(iosIconFullPath); - - // const androidFullPath = path.join(directoryPath, "App_Resources/Android/icon.png"); - // fs.writeFile(androidFullPath, "test-image"); - // created.resources.android.push(androidFullPath); - // }); - - // await execPreparePlatform(platformToTest, testDirData, release); - - // const test1FileName = platformToTest.toLowerCase() === "ios" ? "test1.js" : "test2.js"; - // const test2FileName = platformToTest.toLowerCase() === "ios" ? "test2.js" : "test1.js"; - - // // Asserts that the files in app folder are process as platform specific - // assert.isTrue(fs.exists(path.join(testDirData.appDestFolderPath, "app", test1FileName))); - // assert.isFalse(fs.exists(path.join(testDirData.appDestFolderPath, "app", "test1-js"))); - - // assert.isFalse(fs.exists(path.join(testDirData.appDestFolderPath, "app", test2FileName))); - // assert.isFalse(fs.exists(path.join(testDirData.appDestFolderPath, "app", "test2-js"))); - - // // Asserts that the files in app1 folder aren't process as platform specific - // assert.isFalse(fs.exists(path.join(testDirData.appDestFolderPath, "app1")), "Asserts that the files in app1 folder aren't process as platform specific"); - - // if (release) { - // // Asserts that the files in tests folder aren't copied - // assert.isFalse(fs.exists(path.join(testDirData.appDestFolderPath, "tests")), "Asserts that the files in tests folder aren't copied"); - // } - - // return created; - // } - - // function updateFile(files: string[], fileName: string, content: string) { - // const fileToUpdate = _.find(files, (f) => f.indexOf(fileName) !== -1); - // fs.writeFile(fileToUpdate, content); - // } - - // it("should process only files in app folder when preparing for iOS platform", async () => { - // await testPreparePlatform("iOS"); - // }); - - // it("should process only files in app folder when preparing for Android platform", async () => { - // await testPreparePlatform("Android"); - // }); - - // it("should process only files in app folder when preparing for iOS platform", async () => { - // await testPreparePlatform("iOS", true); - // }); - - // it("should process only files in app folder when preparing for Android platform", async () => { - // await testPreparePlatform("Android", true); - // }); - - // function getDefaultFolderVerificationData(platform: string, appDestFolderPath: string) { - // const data: any = {}; - // if (platform.toLowerCase() === "ios") { - // data[path.join(appDestFolderPath, "app")] = { - // missingFiles: ["test1.ios.js", "test2.android.js", "test2.js"], - // presentFiles: ["test1.js", "test2-android-js", "test1-ios-js", "main.js"] - // }; - - // data[appDestFolderPath] = { - // filesWithContent: [ - // { - // name: "Resources/icon.png", - // content: "test-image" - // } - // ] - // }; - // } else { - // data[path.join(appDestFolderPath, "app")] = { - // missingFiles: ["test1.android.js", "test2.ios.js", "test1.js"], - // presentFiles: ["test2.js", "test2-android-js", "test1-ios-js"] - // }; - - // data[appDestFolderPath] = { - // filesWithContent: [ - // { - // name: "src/main/res/icon.png", - // content: "test-image" - // } - // ] - // }; - // } - - // return data; - // } - - // function mergeModifications(def: any, mod: any) { - // // custom merge to reflect changes - // const merged: any = _.cloneDeep(def); - // _.forOwn(mod, (modFolder, folderRoot) => { - // // whole folder not present in Default - // if (!def.hasOwnProperty(folderRoot)) { - // merged[folderRoot] = _.cloneDeep(modFolder[folderRoot]); - // } else { - // const defFolder = def[folderRoot]; - // merged[folderRoot].filesWithContent = _.merge(defFolder.filesWithContent || [], modFolder.filesWithContent || []); - // merged[folderRoot].missingFiles = (defFolder.missingFiles || []).concat(modFolder.missingFiles || []); - // merged[folderRoot].presentFiles = (defFolder.presentFiles || []).concat(modFolder.presentFiles || []); - - // // remove the missingFiles from the presentFiles if they were initially there - // if (modFolder.missingFiles) { - // merged[folderRoot].presentFiles = _.difference(defFolder.presentFiles, modFolder.missingFiles); - // } - - // // remove the presentFiles from the missingFiles if they were initially there. - // if (modFolder.presentFiles) { - // merged[folderRoot].missingFiles = _.difference(defFolder.presentFiles, modFolder.presentFiles); - // } - // } - // }); - - // return merged; - // } - - // // Executes a changes test case: - // // 1. Executes Prepare Platform for the Platform - // // 2. Applies some changes to the App. Persists the expected Modifications - // // 3. Executes again Prepare Platform for the Platform - // // 4. Gets the Default Destination App Structure and merges it with the Modifications - // // 5. Asserts the Destination App matches our expectations - // async function testChangesApplied(platform: string, applyChangesFn: (createdTestData: CreatedTestData) => any) { - // const createdTestData = await testPreparePlatform(platform); - - // const modifications = applyChangesFn(createdTestData); - - // await execPreparePlatform(platform, createdTestData.testDirData); - - // const defaultStructure = getDefaultFolderVerificationData(platform, createdTestData.testDirData.appDestFolderPath); - - // const merged = mergeModifications(defaultStructure, modifications); - - // DestinationFolderVerifier.verify(merged, fs); - // } - - // it("should sync only changed files, without special folders (iOS)", async () => { - // const applyChangesFn = (createdTestData: CreatedTestData) => { - // // apply changes - // const expectedFileContent = "updated-content-ios"; - // updateFile(createdTestData.files, "test1.ios.js", expectedFileContent); - - // // construct the folder modifications data - // const modifications: any = {}; - // modifications[path.join(createdTestData.testDirData.appDestFolderPath, "app")] = { - // filesWithContent: [ - // { - // name: "test1.js", - // content: expectedFileContent - // } - // ] - // }; - // return modifications; - // }; - // await testChangesApplied("iOS", applyChangesFn); - // }); - - // it("should sync only changed files, without special folders (Android) #2697", async () => { - // const applyChangesFn = (createdTestData: CreatedTestData) => { - // // apply changes - // const expectedFileContent = "updated-content-android"; - // updateFile(createdTestData.files, "test2.android.js", expectedFileContent); - - // // construct the folder modifications data - // const modifications: any = {}; - // modifications[path.join(createdTestData.testDirData.appDestFolderPath, "app")] = { - // filesWithContent: [ - // { - // name: "test2.js", - // content: expectedFileContent - // } - // ] - // }; - // return modifications; - // }; - // await testChangesApplied("Android", applyChangesFn); - // }); - - // it("Ensure App_Resources get reloaded after change in the app folder (iOS) #2560", async () => { - // const applyChangesFn = (createdTestData: CreatedTestData) => { - // // apply changes - // const expectedFileContent = "updated-icon-content"; - // const iconPngPath = path.join(createdTestData.testDirData.appFolderPath, "App_Resources/iOS/icon.png"); - // fs.writeFile(iconPngPath, expectedFileContent); - - // // construct the folder modifications data - // const modifications: any = {}; - // modifications[createdTestData.testDirData.appDestFolderPath] = { - // filesWithContent: [ - // { - // name: "Resources/icon.png", - // content: expectedFileContent - // } - // ] - // }; - - // return modifications; - // }; - // await testChangesApplied("iOS", applyChangesFn); - // }); - - // it("Ensure App_Resources get reloaded after change in the app folder (Android) #2560", async () => { - // const applyChangesFn = (createdTestData: CreatedTestData) => { - // // apply changes - // const expectedFileContent = "updated-icon-content"; - // const iconPngPath = path.join(createdTestData.testDirData.appFolderPath, "App_Resources/Android/icon.png"); - // fs.writeFile(iconPngPath, expectedFileContent); - - // // construct the folder modifications data - // const modifications: any = {}; - // modifications[createdTestData.testDirData.appDestFolderPath] = { - // filesWithContent: [ - // { - // name: "src/main/res/icon.png", - // content: expectedFileContent - // } - // ] - // }; - - // return modifications; - // }; - // await testChangesApplied("Android", applyChangesFn); - // }); - - // it("Ensure App_Resources get reloaded after a new file appears in the app folder (iOS) #2560", async () => { - // const applyChangesFn = (createdTestData: CreatedTestData) => { - // // apply changes - // const expectedFileContent = "new-file-content"; - // const iconPngPath = path.join(createdTestData.testDirData.appFolderPath, "App_Resources/iOS/new-file.png"); - // fs.writeFile(iconPngPath, expectedFileContent); - - // // construct the folder modifications data - // const modifications: any = {}; - // modifications[createdTestData.testDirData.appDestFolderPath] = { - // filesWithContent: [ - // { - // name: "Resources/new-file.png", - // content: expectedFileContent - // } - // ] - // }; - - // return modifications; - // }; - // await testChangesApplied("iOS", applyChangesFn); - // }); - - // it("Ensure App_Resources get reloaded after a new file appears in the app folder (Android) #2560", async () => { - // const applyChangesFn = (createdTestData: CreatedTestData) => { - // // apply changes - // const expectedFileContent = "new-file-content"; - // const iconPngPath = path.join(createdTestData.testDirData.appFolderPath, "App_Resources/Android/new-file.png"); - // fs.writeFile(iconPngPath, expectedFileContent); - - // // construct the folder modifications data - // const modifications: any = {}; - // modifications[createdTestData.testDirData.appDestFolderPath] = { - // filesWithContent: [ - // { - // name: "src/main/res/new-file.png", - // content: expectedFileContent - // } - // ] - // }; - - // return modifications; - // }; - // await testChangesApplied("Android", applyChangesFn); - // }); - - // it("should sync new platform specific files (iOS)", async () => { - // const applyChangesFn = (createdTestData: CreatedTestData) => { - // // apply changes - // const expectedFileContent = "new-content-ios"; - // fs.writeFile(path.join(createdTestData.testDirData.appFolderPath, "test3.ios.js"), expectedFileContent); - - // // construct the folder modifications data - // const modifications: any = {}; - // modifications[path.join(createdTestData.testDirData.appDestFolderPath, "app")] = { - // filesWithContent: [ - // { - // name: "test3.js", - // content: expectedFileContent - // } - // ] - // }; - - // return modifications; - // }; - // await testChangesApplied("iOS", applyChangesFn); - // }); - - // it("should sync new platform specific files (Android)", async () => { - // const applyChangesFn = (createdTestData: CreatedTestData) => { - // // apply changes - // const expectedFileContent = "new-content-android"; - // fs.writeFile(path.join(createdTestData.testDirData.appFolderPath, "test3.android.js"), expectedFileContent); - - // // construct the folder modifications data - // const modifications: any = {}; - // modifications[path.join(createdTestData.testDirData.appDestFolderPath, "app")] = { - // filesWithContent: [ - // { - // name: "test3.js", - // content: expectedFileContent - // } - // ] - // }; - - // return modifications; - // }; - // await testChangesApplied("Android", applyChangesFn); - // }); - - // it("should sync new common files (iOS)", async () => { - // const applyChangesFn = (createdTestData: CreatedTestData) => { - // // apply changes - // const expectedFileContent = "new-content-ios"; - // fs.writeFile(path.join(createdTestData.testDirData.appFolderPath, "test3.js"), expectedFileContent); - - // // construct the folder modifications data - // const modifications: any = {}; - // modifications[path.join(createdTestData.testDirData.appDestFolderPath, "app")] = { - // filesWithContent: [ - // { - // name: "test3.js", - // content: expectedFileContent - // } - // ] - // }; - - // return modifications; - // }; - // await testChangesApplied("iOS", applyChangesFn); - // }); - - // it("should sync new common file (Android)", async () => { - // const applyChangesFn = (createdTestData: CreatedTestData) => { - // // apply changes - // const expectedFileContent = "new-content-android"; - // fs.writeFile(path.join(createdTestData.testDirData.appFolderPath, "test3.js"), expectedFileContent); - - // // construct the folder modifications data - // const modifications: any = {}; - // modifications[path.join(createdTestData.testDirData.appDestFolderPath, "app")] = { - // filesWithContent: [ - // { - // name: "test3.js", - // content: expectedFileContent - // } - // ] - // }; - - // return modifications; - // }; - // await testChangesApplied("Android", applyChangesFn); - // }); - - // it("invalid xml is caught", async () => { - // require("colors"); - // const testDirData = prepareDirStructure(); - - // // generate invalid xml - // const fileFullPath = path.join(testDirData.appFolderPath, "file.xml"); - // fs.writeFile(fileFullPath, ""); - - // const platformsData = testInjector.resolve("platformsData"); - // platformsData.platformsNames = ["android"]; - // platformsData.getPlatformData = (platform: string) => { - // return { - // appDestinationDirectoryPath: testDirData.appDestFolderPath, - // appResourcesDestinationDirectoryPath: testDirData.appResourcesFolderPath, - // normalizedPlatformName: "Android", - // projectRoot: testDirData.tempFolder, - // configurationFileName: "configFileName", - // platformProjectService: { - // prepareProject: (): any => null, - // prepareAppResources: (): any => null, - // validate: () => Promise.resolve(), - // createProject: (projectRoot: string, frameworkDir: string) => Promise.resolve(), - // interpolateData: (projectRoot: string) => Promise.resolve(), - // afterCreateProject: (projectRoot: string): any => null, - // getAppResourcesDestinationDirectoryPath: () => testDirData.appResourcesFolderPath, - // processConfigurationFilesFromAppResources: () => Promise.resolve(), - // handleNativeDependenciesChange: () => Promise.resolve(), - // ensureConfigurationFileInAppResources: (): any => null, - // interpolateConfigurationFile: (): void => undefined, - // isPlatformPrepared: (projectRoot: string) => false, - // checkForChanges: () => { /* */ } - // }, - // frameworkPackageName: "tns-ios" - // }; - // }; - - // const projectData = testInjector.resolve("projectData"); - // projectData.projectDir = testDirData.tempFolder; - // projectData.appDirectoryPath = projectData.getAppDirectoryPath(); - // projectData.appResourcesDirectoryPath = projectData.getAppResourcesDirectoryPath(); - - // platformService = testInjector.resolve("platformService"); - // const oldLoggerWarner = testInjector.resolve("$logger").warn; - // let warnings: string = ""; - // try { - // testInjector.resolve("$logger").warn = (text: string) => warnings += text; - // const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: false, release: false, useHotModuleReload: false }; - // await platformService.preparePlatform({ - // platform: "android", - // appFilesUpdaterOptions, - // platformTemplate: "", - // projectData, - // config: { provision: null, teamId: null, sdk: null, frameworkPath: null, ignoreScripts: false }, - // env: {} - // }); - // } finally { - // testInjector.resolve("$logger").warn = oldLoggerWarner; - // } - - // // Asserts that prepare has caught invalid xml - // assert.isFalse(warnings.indexOf("has errors") !== -1); - // }); - // }); - - describe("build", () => { - function mockData(buildOutput: string[], projectName: string): void { - mockPlatformsData(projectName); - mockFileSystem(buildOutput); - platformService.saveBuildInfoFile = () => undefined; - } - - function mockPlatformsData(projectName: string): void { - const platformsData = testInjector.resolve("platformsData"); - platformsData.getPlatformData = (platform: string) => { - return { - deviceBuildOutputPath: "", - normalizedPlatformName: "", - getBuildOutputPath: () => "", - platformProjectService: { - buildProject: () => Promise.resolve(), - on: () => ({}), - removeListener: () => ({}) - }, - getValidBuildOutputData: () => ({ - packageNames: ["app-debug.apk", "app-release.apk", `${projectName}-debug.apk`, `${projectName}-release.apk`], - regexes: [/app-.*-(debug|release).apk/, new RegExp(`${projectName}-.*-(debug|release).apk`)] - }) - }; - }; - } - - function mockFileSystem(enumeratedFiles: string[]): void { - const fs = testInjector.resolve("fs"); - fs.enumerateFilesInDirectorySync = () => enumeratedFiles; - fs.readDirectory = () => []; - fs.getFsStats = () => (({ mtime: new Date() })); - } - - describe("android platform", () => { - function getTestCases(configuration: string, apkName: string) { - return [{ - name: "no additional options are specified in .gradle file", - buildOutput: [`/my/path/${configuration}/${apkName}-${configuration}.apk`], - expectedResult: `/my/path/${configuration}/${apkName}-${configuration}.apk` - }, { - name: "productFlavors are specified in .gradle file", - buildOutput: [`/my/path/arm64Demo/${configuration}/${apkName}-arm64-demo-${configuration}.apk`, - `/my/path/arm64Full/${configuration}/${apkName}-arm64-full-${configuration}.apk`, - `/my/path/armDemo/${configuration}/${apkName}-arm-demo-${configuration}.apk`, - `/my/path/armFull/${configuration}/${apkName}-arm-full-${configuration}.apk`, - `/my/path/x86Demo/${configuration}/${apkName}-x86-demo-${configuration}.apk`, - `/my/path/x86Full/${configuration}/${apkName}-x86-full-${configuration}.apk`], - expectedResult: `/my/path/x86Full/${configuration}/${apkName}-x86-full-${configuration}.apk` - }, { - name: "split options are specified in .gradle file", - buildOutput: [`/my/path/${configuration}/${apkName}-arm64-v8a-${configuration}.apk`, - `/my/path/${configuration}/${apkName}-armeabi-v7a-${configuration}.apk`, - `/my/path/${configuration}/${apkName}-universal-${configuration}.apk`, - `/my/path/${configuration}/${apkName}-x86-${configuration}.apk`], - expectedResult: `/my/path/${configuration}/${apkName}-x86-${configuration}.apk` - }, { - name: "android-runtime has version < 4.0.0", - buildOutput: [`/my/path/apk/${apkName}-${configuration}.apk`], - expectedResult: `/my/path/apk/${apkName}-${configuration}.apk` - }]; - } - - const platform = "Android"; - const buildConfigs = [{ buildForDevice: false }, { buildForDevice: true }]; - const apkNames = ["app", "testProj"]; - const configurations = ["debug", "release"]; - - _.each(apkNames, apkName => { - _.each(buildConfigs, buildConfig => { - _.each(configurations, configuration => { - _.each(getTestCases(configuration, apkName), testCase => { - it(`should find correct ${configuration} ${apkName}.apk when ${testCase.name} and buildConfig is ${JSON.stringify(buildConfig)}`, async () => { - mockData(testCase.buildOutput, apkName); - const actualResult = await platformService.buildPlatform(platform, buildConfig, { projectName: "" }); - assert.deepEqual(actualResult, testCase.expectedResult); - }); - }); - }); - }); - }); - }); - }); - - describe("ensurePlatformInstalled", () => { - const platform = "android"; - const appFilesUpdaterOptions = { bundle: true }; - let areWebpackFilesPersisted = false; - - let projectData: IProjectData = null; - let usbLiveSyncService: any = null; - let projectChangesService: IProjectChangesService = null; - - beforeEach(() => { - reset(); - - (platformService).addPlatform = () => { /** */ }; - (platformService).persistWebpackFiles = () => areWebpackFilesPersisted = true; - - projectData = testInjector.resolve("projectData"); - usbLiveSyncService = testInjector.resolve("usbLiveSyncService"); - projectChangesService = testInjector.resolve("projectChangesService"); - - usbLiveSyncService.isInitialized = true; - }); - - function reset() { - areWebpackFilesPersisted = false; - } - - function mockPrepareInfo(prepareInfo: any) { - projectChangesService.getPrepareInfo = () => prepareInfo; - } - - const testCases = [ - { - name: "should persist webpack files when prepareInfo is null (first execution of `tns run --bundle`)", - areWebpackFilesPersisted: true - }, - { - name: "should persist webpack files when prepareInfo is null and skipNativePrepare is true (first execution of `tns preview --bundle`)", - nativePrepare: { skipNativePrepare: true }, - areWebpackFilesPersisted: true - }, - { - name: "should not persist webpack files when requires platform add", - prepareInfo: { nativePlatformStatus: constants.NativePlatformStatus.requiresPlatformAdd }, - areWebpackFilesPersisted: true - }, - { - name: "should persist webpack files when requires platform add and skipNativePrepare is true", - prepareInfo: { nativePlatformStatus: constants.NativePlatformStatus.requiresPlatformAdd }, - nativePrepare: { skipNativePrepare: true }, - areWebpackFilesPersisted: false - }, - { - name: "should persist webpack files when platform is already prepared", - prepareInfo: { nativePlatformStatus: constants.NativePlatformStatus.alreadyPrepared }, - areWebpackFilesPersisted: false - }, - { - name: "should not persist webpack files when platform is already prepared and skipNativePrepare is true", - prepareInfo: { nativePlatformStatus: constants.NativePlatformStatus.alreadyPrepared }, - areWebpackFilesPersisted: false - }, - { - name: "should not persist webpack files when no webpack watcher is started (first execution of `tns build --bundle`)", - isWebpackWatcherStarted: false, - areWebpackFilesPersisted: false - }, - { - name: "should not persist webpack files when no webpack watcher is started and skipNativePrepare is true (local JS prepare from cloud command)", - isWebpackWatcherStarted: false, - nativePrepare: { skipNativePrepare: true }, - areWebpackFilesPersisted: false - } - ]; - - _.each(testCases, (testCase: any) => { - it(`${testCase.name}`, async () => { - usbLiveSyncService.isInitialized = testCase.isWebpackWatcherStarted === undefined ? true : testCase.isWebpackWatcherStarted; - mockPrepareInfo(testCase.prepareInfo); - - await (platformService).ensurePlatformInstalled(platform, projectData, config, appFilesUpdaterOptions, testCase.nativePrepare); - assert.deepEqual(areWebpackFilesPersisted, testCase.areWebpackFilesPersisted); - }); - }); - - it("should not persist webpack files after the second execution of `tns preview --bundle` or `tns cloud run --bundle`", async () => { - // First execution of `tns preview --bundle` - mockPrepareInfo(null); - await (platformService).ensurePlatformInstalled(platform, projectData, config, appFilesUpdaterOptions, { skipNativePrepare: true }); - assert.isTrue(areWebpackFilesPersisted); - - // Second execution of `tns preview --bundle` - reset(); - mockPrepareInfo({ nativePlatformStatus: constants.NativePlatformStatus.requiresPlatformAdd }); - await (platformService).ensurePlatformInstalled(platform, projectData, config, appFilesUpdaterOptions, { skipNativePrepare: true }); - assert.isFalse(areWebpackFilesPersisted); - }); - - it("should not persist webpack files after the second execution of `tns run --bundle`", async () => { - // First execution of `tns run --bundle` - mockPrepareInfo(null); - await (platformService).ensurePlatformInstalled(platform, projectData, config, appFilesUpdaterOptions); - assert.isTrue(areWebpackFilesPersisted); - - // Second execution of `tns run --bundle` - reset(); - mockPrepareInfo({ nativePlatformStatus: constants.NativePlatformStatus.alreadyPrepared }); - await (platformService).ensurePlatformInstalled(platform, projectData, config, appFilesUpdaterOptions); - assert.isFalse(areWebpackFilesPersisted); - }); - - it("should handle correctly the following sequence of commands: `tns preview --bundle`, `tns run --bundle` and `tns preview --bundle`", async () => { - // First execution of `tns preview --bundle` - mockPrepareInfo(null); - await (platformService).ensurePlatformInstalled(platform, projectData, config, appFilesUpdaterOptions, { skipNativePrepare: true }); - assert.isTrue(areWebpackFilesPersisted); - - // Execution of `tns run --bundle` - reset(); - mockPrepareInfo({ nativePlatformStatus: constants.NativePlatformStatus.requiresPlatformAdd }); - await (platformService).ensurePlatformInstalled(platform, projectData, config, appFilesUpdaterOptions); - assert.isTrue(areWebpackFilesPersisted); - - // Execution of `tns preview --bundle` - reset(); - mockPrepareInfo({ nativePlatformStatus: constants.NativePlatformStatus.alreadyPrepared }); - await (platformService).ensurePlatformInstalled(platform, projectData, config, appFilesUpdaterOptions, { skipNativePrepare: true }); - assert.isFalse(areWebpackFilesPersisted); - }); - - it("should handle correctly the following sequence of commands: `tns preview --bundle`, `tns run --bundle` and `tns build --bundle`", async () => { - // Execution of `tns preview --bundle` - mockPrepareInfo(null); - await (platformService).ensurePlatformInstalled(platform, projectData, config, appFilesUpdaterOptions, { skipNativePrepare: true }); - assert.isTrue(areWebpackFilesPersisted); - - // Execution of `tns run --bundle` - reset(); - mockPrepareInfo({ nativePlatformStatus: constants.NativePlatformStatus.requiresPlatformAdd }); - await (platformService).ensurePlatformInstalled(platform, projectData, config, appFilesUpdaterOptions); - assert.isTrue(areWebpackFilesPersisted); - - // Execution of `tns build --bundle` - reset(); - mockPrepareInfo({ nativePlatformStatus: constants.NativePlatformStatus.alreadyPrepared }); - await (platformService).ensurePlatformInstalled(platform, projectData, config, appFilesUpdaterOptions); - assert.isFalse(areWebpackFilesPersisted); - }); - }); -}); diff --git a/test/plugins-service.ts b/test/plugins-service.ts index f0ef9708d5..700be07c6c 100644 --- a/test/plugins-service.ts +++ b/test/plugins-service.ts @@ -7,14 +7,13 @@ import { YarnPackageManager } from "../lib/yarn-package-manager"; import { FileSystem } from "../lib/common/file-system"; import { ProjectData } from "../lib/project-data"; import { ChildProcess } from "../lib/common/child-process"; -import { PlatformService } from '../lib/services/platform-service'; import { Options } from "../lib/options"; import { CommandsService } from "../lib/common/services/commands-service"; import { StaticConfig } from "../lib/config"; import { HostInfo } from "../lib/common/host-info"; import { Errors } from "../lib/common/errors"; import { ProjectHelper } from "../lib/common/project-helper"; -import { PlatformsData } from "../lib/platforms-data"; +import { PlatformsDataService } from "../lib/services/platforms-data-service"; import { ProjectDataService } from "../lib/services/project-data-service"; import { ProjectFilesManager } from "../lib/common/services/project-files-manager"; import { ResourceLoader } from "../lib/common/resource-loader"; @@ -55,10 +54,9 @@ function createTestInjector() { testInjector.register("adb", {}); testInjector.register("androidDebugBridgeResultHandler", {}); testInjector.register("projectData", ProjectData); - testInjector.register("platforsmData", stubs.PlatformsDataStub); + testInjector.register("platforsmData", stubs.NativeProjectDataStub); testInjector.register("childProcess", ChildProcess); - testInjector.register("platformService", PlatformService); - testInjector.register("platformsData", PlatformsData); + testInjector.register("platformsDataService", PlatformsDataService); testInjector.register("androidEmulatorServices", {}); testInjector.register("androidToolsInfo", AndroidToolsInfo); testInjector.register("sysInfo", {}); @@ -322,9 +320,9 @@ describe("Plugins service", () => { return [{ name: "" }]; }; - // Mock platformsData - const platformsData = testInjector.resolve("platformsData"); - platformsData.getPlatformData = (platform: string) => { + // Mock platformsDataService + const platformsDataService = testInjector.resolve("platformsDataService"); + platformsDataService.getPlatformData = (platform: string) => { return { appDestinationDirectoryPath: path.join(projectFolder, "platforms", "android"), frameworkPackageName: "tns-android", @@ -546,9 +544,9 @@ describe("Plugins service", () => { const appDestinationDirectoryPath = path.join(projectFolder, "platforms", "android"); - // Mock platformsData - const platformsData = testInjector.resolve("platformsData"); - platformsData.getPlatformData = (platform: string) => { + // Mock platformsDataService + const platformsDataService = testInjector.resolve("platformsDataService"); + platformsDataService.getPlatformData = (platform: string) => { return { appDestinationDirectoryPath: appDestinationDirectoryPath, frameworkPackageName: "tns-android", @@ -592,7 +590,7 @@ describe("Plugins service", () => { }; const unitTestsInjector = new Yok(); - unitTestsInjector.register("platformsData", { + unitTestsInjector.register("platformsDataService", { getPlatformData: (_platform: string, pData: IProjectData) => ({ projectRoot: "projectRoot", platformProjectService: { @@ -638,6 +636,8 @@ describe("Plugins service", () => { unitTestsInjector.register("logger", {}); unitTestsInjector.register("errors", {}); unitTestsInjector.register("injector", unitTestsInjector); + unitTestsInjector.register("mobileHelper", MobileHelper); + unitTestsInjector.register("devicePlatformsConstants", DevicePlatformsConstants); const pluginsService: PluginsService = unitTestsInjector.resolve(PluginsService); testData.pluginsService = pluginsService; diff --git a/test/project-changes-service.ts b/test/project-changes-service.ts index b42b5d318e..a5383ae4ee 100644 --- a/test/project-changes-service.ts +++ b/test/project-changes-service.ts @@ -2,7 +2,7 @@ import * as path from "path"; import { BaseServiceTest } from "./base-service-test"; import temp = require("temp"); import { assert } from "chai"; -import { PlatformsData } from "../lib/platforms-data"; +import { PlatformsDataService } from "../lib/services/platforms-data-service"; import { ProjectChangesService } from "../lib/services/project-changes-service"; import * as Constants from "../lib/constants"; import { FileSystem } from "../lib/common/file-system"; @@ -24,7 +24,7 @@ class ProjectChangesServiceTest extends BaseServiceTest { projectDir: this.projectDir }); - this.injector.register("platformsData", PlatformsData); + this.injector.register("platformsDataService", PlatformsDataService); this.injector.register("androidProjectService", {}); this.injector.register("iOSProjectService", {}); this.injector.register("fs", FileSystem); @@ -54,8 +54,19 @@ class ProjectChangesServiceTest extends BaseServiceTest { return this.injector.resolve("projectData"); } - get platformsData(): any { - return this.injector.resolve("platformsData"); + get getNativeProjectDataService(): any { + return this.injector.resolve("platformsDataService"); + } + + getPlatformData(platform: string): IPlatformData { + return { + projectRoot: path.join(this.projectDir, "platforms", platform.toLowerCase()), + platformProjectService: { + checkForChanges: async (changesInfo: IProjectChangesInfo) => { + changesInfo.signingChanged = true; + } + } + }; } } @@ -69,14 +80,14 @@ describe("Project Changes Service Tests", () => { Constants.PLATFORMS_DIR_NAME ); - serviceTest.platformsData.getPlatformData = + serviceTest.getNativeProjectDataService.getPlatformData = (platform: string) => { if (platform.toLowerCase() === "ios") { return { projectRoot: path.join(platformsDir, platform), get platformProjectService(): any { return { - checkForChanges(changesInfo: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): void { + checkForChanges(changesInfo: IProjectChangesInfo): void { changesInfo.signingChanged = true; } }; @@ -87,7 +98,7 @@ describe("Project Changes Service Tests", () => { projectRoot: path.join(platformsDir, platform), get platformProjectService(): any { return { - checkForChanges(changesInfo: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): void { /* no android changes */ } + checkForChanges(changesInfo: IProjectChangesInfo): void { /* no android changes */ } }; } }; @@ -99,7 +110,7 @@ describe("Project Changes Service Tests", () => { it("Gets the correct Prepare Info path for ios/android", () => { for (const platform of ["ios", "android"]) { const actualPrepareInfoPath = serviceTest.projectChangesService - .getPrepareInfoFilePath(platform, this.projectData); + .getPrepareInfoFilePath(serviceTest.getPlatformData(platform)); const expectedPrepareInfoPath = path.join(serviceTest.projectDir, Constants.PLATFORMS_DIR_NAME, @@ -113,7 +124,7 @@ describe("Project Changes Service Tests", () => { describe("Get Prepare Info", () => { it("Returns empty if file path doesn't exists", () => { for (const platform of ["ios", "android"]) { - const projectInfo = serviceTest.projectChangesService.getPrepareInfo(platform, this.projectData); + const projectInfo = serviceTest.projectChangesService.getPrepareInfo(serviceTest.getPlatformData(platform)); assert.isNull(projectInfo); } @@ -139,7 +150,7 @@ describe("Project Changes Service Tests", () => { fs.writeJson(prepareInfoPath, expectedPrepareInfo); // act - const actualPrepareInfo = serviceTest.projectChangesService.getPrepareInfo(platform, this.projectData); + const actualPrepareInfo = serviceTest.projectChangesService.getPrepareInfo(serviceTest.getPlatformData(platform)); // assert assert.deepEqual(actualPrepareInfo, expectedPrepareInfo); @@ -149,40 +160,20 @@ describe("Project Changes Service Tests", () => { describe("Accumulates Changes From Project Services", () => { it("accumulates changes from the project service", async () => { - const iOSChanges = await serviceTest.projectChangesService.checkForChanges({ - platform: "ios", - projectData: serviceTest.projectData, - projectChangesOptions: { - bundle: false, - release: false, - provision: undefined, - teamId: undefined, - useHotModuleReload: false - } + const iOSChanges = await serviceTest.projectChangesService.checkForChanges(serviceTest.getPlatformData("ios"), serviceTest.projectData, { + provision: undefined, + teamId: undefined }); assert.isTrue(!!iOSChanges.signingChanged, "iOS signingChanged expected to be true"); - - const androidChanges = await serviceTest.projectChangesService.checkForChanges({ - platform: "android", - projectData: serviceTest.projectData, - projectChangesOptions: { - bundle: false, - release: false, - provision: undefined, - teamId: undefined, - useHotModuleReload: false - } - }); - assert.isFalse(!!androidChanges.signingChanged, "Android signingChanged expected to be false"); }); }); describe("setNativePlatformStatus", () => { it("creates prepare info and sets only the native platform status when there isn't an existing prepare info", () => { for (const platform of ["ios", "android"]) { - serviceTest.projectChangesService.setNativePlatformStatus(platform, serviceTest.projectData, { nativePlatformStatus: Constants.NativePlatformStatus.requiresPrepare }); + serviceTest.projectChangesService.setNativePlatformStatus(serviceTest.getPlatformData(platform), { nativePlatformStatus: Constants.NativePlatformStatus.requiresPrepare }); - const actualPrepareInfo = serviceTest.projectChangesService.getPrepareInfo(platform, serviceTest.projectData); + const actualPrepareInfo = serviceTest.projectChangesService.getPrepareInfo(serviceTest.getPlatformData(platform)); assert.deepEqual(actualPrepareInfo, { nativePlatformStatus: Constants.NativePlatformStatus.requiresPrepare }); } @@ -190,23 +181,13 @@ describe("Project Changes Service Tests", () => { it(`shouldn't reset prepare info when native platform status is ${Constants.NativePlatformStatus.alreadyPrepared} and there is existing prepare info`, async () => { for (const platform of ["ios", "android"]) { - await serviceTest.projectChangesService.checkForChanges({ - platform, - projectData: serviceTest.projectData, - projectChangesOptions: { - bundle: false, - release: false, - provision: undefined, - teamId: undefined, - useHotModuleReload: false - } - }); - serviceTest.projectChangesService.savePrepareInfo(platform, serviceTest.projectData); - const prepareInfo = serviceTest.projectChangesService.getPrepareInfo(platform, serviceTest.projectData); - - serviceTest.projectChangesService.setNativePlatformStatus(platform, serviceTest.projectData, { nativePlatformStatus: Constants.NativePlatformStatus.alreadyPrepared }); - - const actualPrepareInfo = serviceTest.projectChangesService.getPrepareInfo(platform, serviceTest.projectData); + await serviceTest.projectChangesService.checkForChanges(serviceTest.getPlatformData(platform), serviceTest.projectData, {}); + serviceTest.projectChangesService.savePrepareInfo(serviceTest.getPlatformData(platform)); + const prepareInfo = serviceTest.projectChangesService.getPrepareInfo(serviceTest.getPlatformData(platform)); + + serviceTest.projectChangesService.setNativePlatformStatus(serviceTest.getPlatformData(platform), { nativePlatformStatus: Constants.NativePlatformStatus.alreadyPrepared }); + + const actualPrepareInfo = serviceTest.projectChangesService.getPrepareInfo(serviceTest.getPlatformData(platform)); prepareInfo.nativePlatformStatus = Constants.NativePlatformStatus.alreadyPrepared; assert.deepEqual(actualPrepareInfo, prepareInfo); } @@ -215,20 +196,10 @@ describe("Project Changes Service Tests", () => { _.each([Constants.NativePlatformStatus.requiresPlatformAdd, Constants.NativePlatformStatus.requiresPrepare], nativePlatformStatus => { it(`should reset prepare info when native platform status is ${nativePlatformStatus} and there is existing prepare info`, async () => { for (const platform of ["ios", "android"]) { - await serviceTest.projectChangesService.checkForChanges({ - platform, - projectData: serviceTest.projectData, - projectChangesOptions: { - bundle: false, - release: false, - provision: undefined, - teamId: undefined, - useHotModuleReload: false - } - }); - serviceTest.projectChangesService.setNativePlatformStatus(platform, serviceTest.projectData, { nativePlatformStatus: nativePlatformStatus }); + await serviceTest.projectChangesService.checkForChanges(serviceTest.getPlatformData(platform), serviceTest.projectData, {}); + serviceTest.projectChangesService.setNativePlatformStatus(serviceTest.getPlatformData(platform), { nativePlatformStatus: nativePlatformStatus }); - const actualPrepareInfo = serviceTest.projectChangesService.getPrepareInfo(platform, serviceTest.projectData); + const actualPrepareInfo = serviceTest.projectChangesService.getPrepareInfo(serviceTest.getPlatformData(platform)); assert.deepEqual(actualPrepareInfo, { nativePlatformStatus: nativePlatformStatus }); } }); diff --git a/test/project-files-provider.ts b/test/project-files-provider.ts index 799d8141e7..00e4857236 100644 --- a/test/project-files-provider.ts +++ b/test/project-files-provider.ts @@ -17,7 +17,7 @@ function createTestInjector(): IInjector { testInjector.register('projectData', stubs.ProjectDataStub); - testInjector.register("platformsData", { + testInjector.register("platformsDataService", { getPlatformData: (platform: string) => { return { appDestinationDirectoryPath: appDestinationDirectoryPath, diff --git a/test/services/android-plugin-build-service.ts b/test/services/android-plugin-build-service.ts index 304c0ea6ee..962a0cdcd0 100644 --- a/test/services/android-plugin-build-service.ts +++ b/test/services/android-plugin-build-service.ts @@ -68,11 +68,6 @@ describe('androidPluginBuildService', () => { return null; } }); - testInjector.register('platformService', { - getCurrentPlatformVersion: (platform: string, projectData: IProjectData): string => { - return options.addProjectRuntime ? "1.0.0" : null; - } - }); testInjector.register('packageManager', setupNpm(options)); testInjector.register('filesHashService', { generateHashes: async (files: string[]): Promise => ({}), @@ -314,8 +309,8 @@ dependencies { }); it('builds aar with the specified runtime gradle versions when the project runtime has gradle versions', async () => { - const expectedGradleVersion = "2.2.2"; - const expectedAndroidVersion = "3.3"; + const expectedGradleVersion = "4.4.4"; + const expectedAndroidVersion = "5.5.5"; const config: IPluginBuildOptions = setup({ addManifest: true, addProjectDir: true, diff --git a/test/services/android-project-service.ts b/test/services/android-project-service.ts index 242a9be3be..96bdc0a128 100644 --- a/test/services/android-project-service.ts +++ b/test/services/android-project-service.ts @@ -94,7 +94,7 @@ describe("androidDeviceDebugService", () => { const buildConfig = getDefautlBuildConfig(); //act - await androidProjectService.buildProject("local/local", projectData, buildConfig); + await androidProjectService.buildProject("local/local", projectData, buildConfig); //assert assert.include(childProcess.lastCommandArgs, "assembleRelease"); @@ -106,7 +106,7 @@ describe("androidDeviceDebugService", () => { buildConfig.release = false; //act - await androidProjectService.buildProject("local/local", projectData, buildConfig); + await androidProjectService.buildProject("local/local", projectData, buildConfig); //assert assert.include(childProcess.lastCommandArgs, "assembleDebug"); @@ -118,7 +118,7 @@ describe("androidDeviceDebugService", () => { buildConfig.androidBundle = true; //act - await androidProjectService.buildProject("local/local", projectData, buildConfig); + await androidProjectService.buildProject("local/local", projectData, buildConfig); //assert assert.include(childProcess.lastCommandArgs, "bundleRelease"); @@ -131,7 +131,7 @@ describe("androidDeviceDebugService", () => { buildConfig.release = false; //act - await androidProjectService.buildProject("local/local", projectData, buildConfig); + await androidProjectService.buildProject("local/local", projectData, buildConfig); //assert assert.include(childProcess.lastCommandArgs, "bundleDebug"); diff --git a/test/services/ios-device-debug-service.ts b/test/services/ios-device-debug-service.ts index d97f37863d..df52aaf7cd 100644 --- a/test/services/ios-device-debug-service.ts +++ b/test/services/ios-device-debug-service.ts @@ -26,7 +26,6 @@ class IOSDeviceDebugServiceInheritor extends IOSDeviceDebugService { const createTestInjector = (): IInjector => { const testInjector = new Yok(); testInjector.register("devicesService", {}); - testInjector.register("platformService", {}); testInjector.register("iOSEmulatorServices", {}); testInjector.register("childProcess", {}); diff --git a/test/services/livesync-service.ts b/test/services/livesync-service.ts deleted file mode 100644 index 9a4008ce8c..0000000000 --- a/test/services/livesync-service.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { Yok } from "../../lib/common/yok"; -import { assert } from "chai"; -import { LiveSyncService, DeprecatedUsbLiveSyncService } from "../../lib/services/livesync/livesync-service"; -import { LoggerStub } from "../stubs"; - -const createTestInjector = (): IInjector => { - const testInjector = new Yok(); - - testInjector.register("platformService", {}); - testInjector.register("hmrStatusService", {}); - testInjector.register("projectDataService", { - getProjectData: (projectDir: string): IProjectData => ({}) - }); - - testInjector.register("devicesService", {}); - testInjector.register("mobileHelper", {}); - testInjector.register("devicePlatformsConstants", {}); - testInjector.register("nodeModulesDependenciesBuilder", {}); - testInjector.register("logger", LoggerStub); - testInjector.register("debugService", {}); - testInjector.register("errors", {}); - testInjector.register("debugDataService", {}); - testInjector.register("hooksService", { - executeAfterHooks: (commandName: string, hookArguments?: IDictionary): Promise => Promise.resolve() - }); - - testInjector.register("pluginsService", {}); - testInjector.register("analyticsService", {}); - testInjector.register("injector", testInjector); - testInjector.register("usbLiveSyncService", { - isInitialized: false - }); - testInjector.register("platformsData", { - availablePlatforms: { - Android: "Android", - iOS: "iOS" - } - }); - testInjector.register("previewAppLiveSyncService", {}); - testInjector.register("previewQrCodeService", {}); - testInjector.register("previewSdkService", {}); - - return testInjector; -}; - -class LiveSyncServiceInheritor extends LiveSyncService { - constructor($platformService: IPlatformService, - $projectDataService: IProjectDataService, - $devicesService: Mobile.IDevicesService, - $mobileHelper: Mobile.IMobileHelper, - $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder, - $logger: ILogger, - $hooksService: IHooksService, - $pluginsService: IPluginsService, - $debugService: IDebugService, - $errors: IErrors, - $debugDataService: IDebugDataService, - $analyticsService: IAnalyticsService, - $usbLiveSyncService: DeprecatedUsbLiveSyncService, - $injector: IInjector, - $previewAppLiveSyncService: IPreviewAppLiveSyncService, - $previewQrCodeService: IPreviewQrCodeService, - $previewSdkService: IPreviewSdkService, - $hmrStatusService: IHmrStatusService, - $platformsData: IPlatformsData) { - - super( - $platformService, - $projectDataService, - $devicesService, - $mobileHelper, - $devicePlatformsConstants, - $nodeModulesDependenciesBuilder, - $logger, - $hooksService, - $pluginsService, - $debugService, - $errors, - $debugDataService, - $analyticsService, - $usbLiveSyncService, - $previewAppLiveSyncService, - $previewQrCodeService, - $previewSdkService, - $hmrStatusService, - $injector - ); - } - - public liveSyncProcessesInfo: IDictionary = {}; -} - -interface IStopLiveSyncTestCase { - name: string; - currentDeviceIdentifiers: string[]; - expectedDeviceIdentifiers: string[]; - deviceIdentifiersToBeStopped?: string[]; -} - -describe("liveSyncService", () => { - describe("stopLiveSync", () => { - const getLiveSyncProcessInfo = (): ILiveSyncProcessInfo => ({ - actionsChain: Promise.resolve(), - currentSyncAction: Promise.resolve(), - isStopped: false, - timer: setTimeout(() => undefined, 1000), - watcherInfo: { - watcher: { - close: (): any => undefined - }, - patterns: ["pattern"] - }, - deviceDescriptors: [], - syncToPreviewApp: false - }); - - const getDeviceDescriptor = (identifier: string): ILiveSyncDeviceInfo => ({ - identifier, - outputPath: "", - skipNativePrepare: false, - platformSpecificOptions: null, - buildAction: () => Promise.resolve("") - }); - - const testCases: IStopLiveSyncTestCase[] = [ - { - name: "stops LiveSync operation for all devices and emits liveSyncStopped for all of them when stopLiveSync is called without deviceIdentifiers", - currentDeviceIdentifiers: ["device1", "device2", "device3"], - expectedDeviceIdentifiers: ["device1", "device2", "device3"] - }, - { - name: "stops LiveSync operation for all devices and emits liveSyncStopped for all of them when stopLiveSync is called without deviceIdentifiers (when a single device is attached)", - currentDeviceIdentifiers: ["device1"], - expectedDeviceIdentifiers: ["device1"] - }, - { - name: "stops LiveSync operation for specified devices and emits liveSyncStopped for each of them (when a single device is attached)", - currentDeviceIdentifiers: ["device1"], - expectedDeviceIdentifiers: ["device1"], - deviceIdentifiersToBeStopped: ["device1"] - }, - { - name: "stops LiveSync operation for specified devices and emits liveSyncStopped for each of them", - currentDeviceIdentifiers: ["device1", "device2", "device3"], - expectedDeviceIdentifiers: ["device1", "device3"], - deviceIdentifiersToBeStopped: ["device1", "device3"] - }, - { - name: "does not raise liveSyncStopped event for device, which is not currently being liveSynced", - currentDeviceIdentifiers: ["device1", "device2", "device3"], - expectedDeviceIdentifiers: ["device1"], - deviceIdentifiersToBeStopped: ["device1", "device4"] - } - ]; - - for (const testCase of testCases) { - it(testCase.name, async () => { - const testInjector = createTestInjector(); - const liveSyncService = testInjector.resolve(LiveSyncServiceInheritor); - const projectDir = "projectDir"; - const emittedDeviceIdentifiersForLiveSyncStoppedEvent: string[] = []; - liveSyncService.on("liveSyncStopped", (data: { projectDir: string, deviceIdentifier: string }) => { - assert.equal(data.projectDir, projectDir); - emittedDeviceIdentifiersForLiveSyncStoppedEvent.push(data.deviceIdentifier); - }); - - // Setup liveSyncProcessesInfo for current test - liveSyncService.liveSyncProcessesInfo[projectDir] = getLiveSyncProcessInfo(); - const deviceDescriptors = testCase.currentDeviceIdentifiers.map(d => getDeviceDescriptor(d)); - liveSyncService.liveSyncProcessesInfo[projectDir].deviceDescriptors.push(...deviceDescriptors); - - await liveSyncService.stopLiveSync(projectDir, testCase.deviceIdentifiersToBeStopped); - - assert.deepEqual(emittedDeviceIdentifiersForLiveSyncStoppedEvent, testCase.expectedDeviceIdentifiers); - }); - } - - const prepareTestForUsbLiveSyncService = (): any => { - const testInjector = createTestInjector(); - const liveSyncService = testInjector.resolve(LiveSyncServiceInheritor); - const projectDir = "projectDir"; - const usbLiveSyncService = testInjector.resolve("usbLiveSyncService"); - usbLiveSyncService.isInitialized = true; - - // Setup liveSyncProcessesInfo for current test - liveSyncService.liveSyncProcessesInfo[projectDir] = getLiveSyncProcessInfo(); - const deviceDescriptors = ["device1", "device2", "device3"].map(d => getDeviceDescriptor(d)); - liveSyncService.liveSyncProcessesInfo[projectDir].deviceDescriptors.push(...deviceDescriptors); - return { projectDir, liveSyncService, usbLiveSyncService }; - }; - - it("sets usbLiveSyncService.isInitialized to false when LiveSync is stopped for all devices", async () => { - const { projectDir, liveSyncService, usbLiveSyncService } = prepareTestForUsbLiveSyncService(); - await liveSyncService.stopLiveSync(projectDir, ["device1", "device2", "device3"]); - - assert.isFalse(usbLiveSyncService.isInitialized, "When the LiveSync process is stopped, we must set usbLiveSyncService.isInitialized to false"); - }); - - it("does not set usbLiveSyncService.isInitialized to false when LiveSync is stopped for some of devices only", async () => { - const { projectDir, liveSyncService, usbLiveSyncService } = prepareTestForUsbLiveSyncService(); - await liveSyncService.stopLiveSync(projectDir, ["device1", "device2"]); - - assert.isTrue(usbLiveSyncService.isInitialized, "When the LiveSync process is stopped only for some of the devices, we must not set usbLiveSyncService.isInitialized to false"); - }); - - }); - -}); diff --git a/test/services/livesync/android-device-livesync-service-base.ts b/test/services/livesync/android-device-livesync-service-base.ts index b8e41cd7c3..f79a831af4 100644 --- a/test/services/livesync/android-device-livesync-service-base.ts +++ b/test/services/livesync/android-device-livesync-service-base.ts @@ -32,11 +32,11 @@ const appIdentifier = "testAppIdentifier"; class AndroidDeviceLiveSyncServiceBaseMock extends AndroidDeviceLiveSyncServiceBase { constructor($injector: IInjector, - $platformsData: any, + $platformsDataService: any, $filesHashService: any, $logger: ILogger, device: Mobile.IAndroidDevice) { - super($injector, $platformsData, $filesHashService, $logger, device); + super($injector, $platformsDataService, $filesHashService, $logger, device); } public async transferFilesOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { diff --git a/test/services/platform/add-platform-service.ts b/test/services/platform/add-platform-service.ts new file mode 100644 index 0000000000..8a2dcc9810 --- /dev/null +++ b/test/services/platform/add-platform-service.ts @@ -0,0 +1,77 @@ +import { InjectorStub } from "../../stubs"; +import { AddPlatformService } from "../../../lib/services/platform/add-platform-service"; +import { PacoteService } from "../../../lib/services/pacote-service"; +import { assert } from "chai"; + +const nativePrepare: INativePrepare = null; + +function createTestInjector() { + const injector = new InjectorStub(); + injector.register("pacoteService", { + extractPackage: async (name: string) => ({}) + }); + injector.register("terminalSpinnerService", { + createSpinner: () => { + return { + start: () => ({}), + stop: () => ({}) + }; + } + }); + injector.register("addPlatformService", AddPlatformService); + + const fs = injector.resolve("fs"); + fs.exists = () => false; + + return injector; +} + +describe("AddPlatformService", () => { + describe("addPlatform", () => { + let injector: IInjector, addPlatformService: AddPlatformService, projectData: IProjectData; + beforeEach(() => { + injector = createTestInjector(); + addPlatformService = injector.resolve("addPlatformService"); + projectData = injector.resolve("projectData"); + }); + + _.each(["ios", "android"], platform => { + it(`should fail if unable to extract runtime package for ${platform}`, async () => { + const errorMessage = "Pacote service unable to extract package"; + + const pacoteService: PacoteService = injector.resolve("pacoteService"); + pacoteService.extractPackage = async (): Promise => { throw new Error(errorMessage); }; + + const platformsDataService = injector.resolve("platformsDataService").getPlatformData(platform, projectData); + + await assert.isRejected(addPlatformService.addPlatformSafe(projectData, platformsDataService, "somePackage", nativePrepare), errorMessage); + }); + it(`shouldn't add native platform when skipNativePrepare is provided for ${platform}`, async () => { + const projectDataService = injector.resolve("projectDataService"); + projectDataService.getNSValue = () => ({ version: "4.2.0" }); + + let isCreateNativeProjectCalled = false; + const platformsDataService = injector.resolve("platformsDataService"); + const platformData = platformsDataService.getPlatformData(platform, injector.resolve("projectData")); + platformData.platformProjectService.createProject = () => isCreateNativeProjectCalled = true; + platformsDataService.getPlatformData = () => platformData; + + await addPlatformService.addPlatformSafe(projectData, platformData, platform, { skipNativePrepare: true } ); + assert.isFalse(isCreateNativeProjectCalled); + }); + it(`should add native platform when skipNativePrepare is not provided for ${platform}`, async () => { + const projectDataService = injector.resolve("projectDataService"); + projectDataService.getNSValue = () => ({ version: "4.2.0" }); + + let isCreateNativeProjectCalled = false; + const platformsDataService = injector.resolve("platformsDataService"); + const platformData = platformsDataService.getPlatformData(platform, injector.resolve("projectData")); + platformData.platformProjectService.createProject = () => isCreateNativeProjectCalled = true; + platformsDataService.getPlatformData = () => platformData; + + await addPlatformService.addPlatformSafe(projectData, platformData, platform, nativePrepare); + assert.isTrue(isCreateNativeProjectCalled); + }); + }); + }); +}); diff --git a/test/services/playground/preview-app-files-service.ts b/test/services/playground/preview-app-files-service.ts index c58dd13702..b32855fcb0 100644 --- a/test/services/playground/preview-app-files-service.ts +++ b/test/services/playground/preview-app-files-service.ts @@ -17,7 +17,7 @@ class ProjectDataServiceMock { } } -class PlatformsDataMock { +class NativeProjectDataServiceMock { public getPlatformData(platform: string) { const appDestinationDirectoryPath = path.join(projectDir, "platforms", platform, "app"); return { @@ -31,7 +31,7 @@ function createTestInjector(data?: { files: string[] }) { injector.register("previewAppFilesService", PreviewAppFilesService); injector.register("fs", FileSystemStub); injector.register("logger", LoggerStub); - injector.register("platformsData", PlatformsDataMock); + injector.register("platformsDataService", NativeProjectDataServiceMock); injector.register("projectDataService", ProjectDataServiceMock); injector.register("projectFilesManager", { getProjectFiles: () => data ? data.files : [] @@ -48,12 +48,11 @@ function createTestInjector(data?: { files: string[] }) { } function getExpectedResult(data: IPreviewAppLiveSyncData, injector: IInjector, expectedFiles: string[], platform: string): FilesPayload { - const projectData = injector.resolve("projectDataService").getProjectData(); - const platformData = injector.resolve("platformsData").getPlatformData(platform); + const platformData = injector.resolve("platformsDataService").getPlatformData(platform); const files = _.map(expectedFiles, expectedFile => { return { event: 'change', - file: data.bundle ? path.relative(path.join(platformData.appDestinationDirectoryPath, "app"), expectedFile) : path.relative(projectData.appDirectoryPath(), expectedFile), + file: path.relative(path.join(platformData.appDestinationDirectoryPath, "app"), expectedFile), binary: false, fileContents: undefined }; diff --git a/test/services/playground/preview-app-livesync-service.ts b/test/services/playground/preview-app-livesync-service.ts index 4706a00439..fea024d29d 100644 --- a/test/services/playground/preview-app-livesync-service.ts +++ b/test/services/playground/preview-app-livesync-service.ts @@ -2,12 +2,19 @@ import { Yok } from "../../../lib/common/yok"; import * as _ from 'lodash'; import { LoggerStub, ErrorsStub } from "../../stubs"; import { FilePayload, Device, FilesPayload } from "nativescript-preview-sdk"; -import { PreviewAppLiveSyncService } from "../../../lib/services/livesync/playground/preview-app-livesync-service"; import * as chai from "chai"; import * as path from "path"; import { ProjectFilesManager } from "../../../lib/common/services/project-files-manager"; import { EventEmitter } from "events"; import { PreviewAppFilesService } from "../../../lib/services/livesync/playground/preview-app-files-service"; +import { PREPARE_READY_EVENT_NAME } from "../../../lib/constants"; +import { PrepareData } from "../../../lib/data/prepare-data"; +import { PreviewAppController } from "../../../lib/controllers/preview-app-controller"; +import { PreviewAppEmitter } from "../../../lib/emitters/preview-app-emitter"; +import { PrepareDataService } from "../../../lib/services/prepare-data-service"; +import { MobileHelper } from "../../../lib/common/mobile/mobile-helper"; +import { DevicePlatformsConstants } from "../../../lib/common/mobile/device-platforms-constants"; +import { PrepareController } from "../../../lib/controllers/prepare-controller"; interface ITestCase { name: string; @@ -30,14 +37,15 @@ interface IAssertOptions { } interface IActInput { - previewAppLiveSyncService?: IPreviewAppLiveSyncService; + previewAppController?: PreviewAppController; previewSdkService?: PreviewSdkServiceMock; + prepareController?: PrepareController; projectFiles?: string[]; actOptions?: IActOptions; } let isComparePluginsOnDeviceCalled = false; -let isHookCalledWithHMR = false; +let isHMRPassedToEnv = false; let applyChangesParams: FilePayload[] = []; let initialFiles: FilePayload[] = []; let readTextParams: string[] = []; @@ -95,6 +103,13 @@ class LoggerMock extends LoggerStub { } } +class PrepareControllerMock extends EventEmitter { + public prepare(prepareData: PrepareData) { + isHMRPassedToEnv = prepareData.env.hmr; + this.emit(PREPARE_READY_EVENT_NAME, { hmrData: {}, files: [] }); + } +} + function createTestInjector(options?: { projectFiles?: string[] }) { @@ -102,9 +117,11 @@ function createTestInjector(options?: { const injector = new Yok(); injector.register("logger", LoggerMock); - injector.register("hmrStatusService", {}); + injector.register("hmrStatusService", { + attachToHmrStatusEvent: () => ({}) + }); injector.register("errors", ErrorsStub); - injector.register("platformsData", { + injector.register("platformsDataService", { getPlatformData: () => ({ appDestinationDirectoryPath: platformsDirPath, normalizedPlatformName @@ -125,7 +142,15 @@ function createTestInjector(options?: { getExternalPlugins: () => [] }); injector.register("projectFilesManager", ProjectFilesManager); - injector.register("previewAppLiveSyncService", PreviewAppLiveSyncService); + injector.register("previewAppLiveSyncService", { + syncFilesForPlatformSafe: () => ({}) + }); + injector.register("previewAppEmitter", PreviewAppEmitter); + injector.register("previewAppController", PreviewAppController); + injector.register("prepareController", PrepareControllerMock); + injector.register("prepareDataService", PrepareDataService); + injector.register("mobileHelper", MobileHelper); + injector.register("devicePlatformsConstants", DevicePlatformsConstants); injector.register("fs", { readText: (filePath: string) => { readTextParams.push(filePath); @@ -149,11 +174,6 @@ function createTestInjector(options?: { }, mapFilePath: (filePath: string) => path.join(path.join(platformsDirPath, "app"), path.relative(path.join(projectDirPath, "app"), filePath)) }); - injector.register("hooksService", { - executeBeforeHooks: (name: string, args: any) => { - isHookCalledWithHMR = args.hookArgs.config.appFilesUpdaterOptions.useHotModuleReload; - } - }); injector.register("previewDevicesService", { getConnectedDevices: () => [deviceMockData] }); @@ -169,22 +189,24 @@ function arrange(options?: { projectFiles?: string[] }) { options = options || {}; const injector = createTestInjector({ projectFiles: options.projectFiles }); - const previewAppLiveSyncService: IPreviewAppLiveSyncService = injector.resolve("previewAppLiveSyncService"); const previewSdkService: IPreviewSdkService = injector.resolve("previewSdkService"); + const previewAppController: PreviewAppController = injector.resolve("previewAppController"); + const prepareController: PrepareController = injector.resolve("prepareController"); return { - previewAppLiveSyncService, - previewSdkService + previewSdkService, + previewAppController, + prepareController, }; } async function initialSync(input?: IActInput) { input = input || {}; - const { previewAppLiveSyncService, previewSdkService, actOptions } = input; + const { previewAppController, previewSdkService, actOptions } = input; const syncFilesData = _.cloneDeep(syncFilesMockData); syncFilesData.useHotModuleReload = actOptions.hmr; - await previewAppLiveSyncService.initialize(syncFilesData); + await previewAppController.preview(syncFilesData); if (actOptions.callGetInitialFiles) { await previewSdkService.getInitialFiles(deviceMockData); } @@ -193,23 +215,23 @@ async function initialSync(input?: IActInput) { async function syncFiles(input?: IActInput) { input = input || {}; - const { previewAppLiveSyncService, previewSdkService, projectFiles, actOptions } = input; + const { previewAppController, previewSdkService, prepareController, projectFiles, actOptions } = input; const syncFilesData = _.cloneDeep(syncFilesMockData); syncFilesData.useHotModuleReload = actOptions.hmr; - await previewAppLiveSyncService.initialize(syncFilesData); + await previewAppController.preview(syncFilesData); if (actOptions.callGetInitialFiles) { await previewSdkService.getInitialFiles(deviceMockData); } - await previewAppLiveSyncService.syncFiles(syncFilesMockData, projectFiles, []); + prepareController.emit(PREPARE_READY_EVENT_NAME, { files: projectFiles }); } async function assert(expectedFiles: string[], options?: IAssertOptions) { options = options || {}; const actualFiles = options.checkInitialFiles ? initialFiles : applyChangesParams; - chai.assert.equal(isHookCalledWithHMR, options.hmr || false); + chai.assert.equal(isHMRPassedToEnv, options.hmr || false); chai.assert.deepEqual(actualFiles, mapFiles(expectedFiles)); if (options.checkWarnings) { @@ -223,7 +245,7 @@ async function assert(expectedFiles: string[], options?: IAssertOptions) { function reset() { isComparePluginsOnDeviceCalled = false; - isHookCalledWithHMR = false; + isHMRPassedToEnv = false; applyChangesParams = []; initialFiles = []; readTextParams = []; @@ -238,7 +260,7 @@ function mapFiles(files: string[]): FilePayload[] { return files.map(file => { return { event: "change", - file: path.join("..", "platforms", "app", file), + file, fileContents: undefined, binary: false }; @@ -267,14 +289,14 @@ function execute(options: { it(`${testCase.name}`, async () => { const projectFiles = testCase.appFiles ? testCase.appFiles.map(file => path.join(projectDirPath, "app", file)) : null; - const { previewAppLiveSyncService, previewSdkService } = arrange({ projectFiles }); - await act.apply(null, [{ previewAppLiveSyncService, previewSdkService, projectFiles, actOptions: testCase.actOptions }]); + const { previewAppController, prepareController, previewSdkService } = arrange({ projectFiles }); + await act.apply(null, [{ previewAppController, prepareController, previewSdkService, projectFiles, actOptions: testCase.actOptions }]); await assert(testCase.expectedFiles, testCase.assertOptions); }); }); } -describe("previewAppLiveSyncService", () => { +describe("previewAppController", () => { describe("initialSync", () => { afterEach(() => reset()); @@ -295,29 +317,6 @@ describe("previewAppLiveSyncService", () => { describe("syncFiles", () => { afterEach(() => reset()); - const nativeFilesTestCases: ITestCase[] = [ - { - name: "Android manifest is changed", - appFiles: ["App_Resources/Android/src/main/AndroidManifest.xml"], - expectedFiles: [] - }, - { - name: "Android app.gradle is changed", - appFiles: ["App_Resources/Android/app.gradle"], - expectedFiles: [] - }, - { - name: "iOS Info.plist is changed", - appFiles: ["App_Resources/iOS/Info.plist"], - expectedFiles: [] - }, - { - name: "iOS build.xcconfig is changed", - appFiles: ["App_Resources/iOS/build.xcconfig"], - expectedFiles: [] - } - ]; - const hmrTestCases: ITestCase[] = [ { name: "when set to true", @@ -356,19 +355,12 @@ describe("previewAppLiveSyncService", () => { ]; const testCategories = [ - { - name: "should show warning and not transfer native files when", - testCases: nativeFilesTestCases.map(testCase => { - testCase.assertOptions = { checkWarnings: true }; - return testCase; - }) - }, { name: "should handle correctly when no files are provided", testCases: noAppFilesTestCases }, { - name: "should pass the hmr option to the hook", + name: "should pass the hmr option to the env", testCases: hmrTestCases } ]; diff --git a/test/services/playground/preview-app-plugins-service.ts b/test/services/playground/preview-app-plugins-service.ts index 5e239819cc..75c52f4bf6 100644 --- a/test/services/playground/preview-app-plugins-service.ts +++ b/test/services/playground/preview-app-plugins-service.ts @@ -86,144 +86,6 @@ function setup(localPlugins: IStringDictionary, previewAppPlugins: IStringDictio } describe("previewAppPluginsService", () => { - describe("comparePluginsOnDevice without bundle", () => { - const testCases = [ - { - name: "should show warning for plugin not included in preview app", - localPlugins: { - "nativescript-facebook": "2.2.3", - "nativescript-theme-core": "~1.0.4", - "tns-core-modules": "~4.2.0" - }, - previewAppPlugins: { - "nativescript-theme-core": "~1.0.4", - "tns-core-modules": "~4.2.0" - }, - expectedWarnings: [ - util.format(PluginComparisonMessages.PLUGIN_NOT_INCLUDED_IN_PREVIEW_APP, "nativescript-facebook", deviceId) - ] - }, - { - name: "should show warnings for plugins not included in preview app", - localPlugins: { - "nativescript-facebook": "2.2.3", - "nativescript-theme-core": "~1.0.4", - "tns-core-modules": "~4.2.0" - }, - previewAppPlugins: { - }, - expectedWarnings: [ - util.format(PluginComparisonMessages.PLUGIN_NOT_INCLUDED_IN_PREVIEW_APP, "nativescript-facebook", deviceId), - util.format(PluginComparisonMessages.PLUGIN_NOT_INCLUDED_IN_PREVIEW_APP, "nativescript-theme-core", deviceId), - util.format(PluginComparisonMessages.PLUGIN_NOT_INCLUDED_IN_PREVIEW_APP, "tns-core-modules", deviceId) - ] - }, - { - name: "should not show warnings when all plugins are included in preview app", - localPlugins: { - "nativescript-theme-core": "1.0.4", - "nativescript-facebook": "2.2.3" - }, - previewAppPlugins: { - "nativescript-theme-core": "1.1.4", - "nativescript-facebook": "2.2.3" - }, - expectedWarnings: [] - }, - { - name: "should show warning when local plugin has lower major version", - localPlugins: { - "nativescript-theme-core": "2.0.0" - }, - previewAppPlugins: { - "nativescript-theme-core": "3.4.0" - }, - expectedWarnings: [ - util.format(PluginComparisonMessages.LOCAL_PLUGIN_WITH_DIFFERENCE_IN_MAJOR_VERSION, "nativescript-theme-core", "2.0.0", "3.4.0") - ] - }, - { - name: "should show warning when local plugin has greater major version", - localPlugins: { - "nativescript-theme-core": "4.0.0" - }, - previewAppPlugins: { - "nativescript-theme-core": "3.0.0" - }, - expectedWarnings: [ - util.format(PluginComparisonMessages.LOCAL_PLUGIN_WITH_DIFFERENCE_IN_MAJOR_VERSION, "nativescript-theme-core", "4.0.0", "3.0.0") - ] - }, - { - name: "should show warning when local plugin has greater minor version and the same major version", - localPlugins: { - "nativescript-theme-core": "3.5.0" - }, - previewAppPlugins: { - "nativescript-theme-core": "3.0.0" - }, - expectedWarnings: [ - util.format(PluginComparisonMessages.LOCAL_PLUGIN_WITH_GREATHER_MINOR_VERSION, "nativescript-theme-core", "3.5.0", "3.0.0") - ] - }, - { - name: "should not show warning when local plugin has lower minor version and the same major version", - localPlugins: { - "nativescript-theme-core": "3.1.0" - }, - previewAppPlugins: { - "nativescript-theme-core": "3.2.0" - }, - expectedWarnings: [] - }, - { - name: "should not show warning when plugins differ only in patch versions (lower local patch version)", - localPlugins: { - "nativescript-theme-core": "3.5.0" - }, - previewAppPlugins: { - "nativescript-theme-core": "3.5.1" - }, - expectedWarnings: [] - }, - { - name: "should not show warning when plugins differ only in patch versions (greater local patch version)", - localPlugins: { - "nativescript-theme-core": "3.5.1" - }, - previewAppPlugins: { - "nativescript-theme-core": "3.5.0" - }, - expectedWarnings: [] - }, - { - name: "should not show warning when the local plugin version is tag", - localPlugins: { - "tns-core-modules": "rc" - }, - previewAppPlugins: { - "tns-core-modules": "5.0.0" - }, - expectedWarnings: [] - } - ]; - - afterEach(() => { - warnParams = []; - readJsonParams = []; - }); - - for (const testCase of testCases) { - it(`${testCase.name}`, async () => { - const { previewAppPluginsService, device } = setup(testCase.localPlugins, testCase.previewAppPlugins); - - await previewAppPluginsService.comparePluginsOnDevice(createPreviewLiveSyncData({ bundle: false }), device); - - assert.equal(warnParams.length, testCase.expectedWarnings.length); - testCase.expectedWarnings.forEach(warning => assert.include(warnParams, warning)); - }); - } - }); describe("comparePluginsOnDevice with bundle", () => { const testCases = [ { diff --git a/test/services/test-execution-serice.ts b/test/services/test-execution-service.ts similarity index 98% rename from test/services/test-execution-serice.ts rename to test/services/test-execution-service.ts index 9d9db7d547..b533da0358 100644 --- a/test/services/test-execution-serice.ts +++ b/test/services/test-execution-service.ts @@ -8,6 +8,7 @@ const unitTestsPluginName = "nativescript-unit-test-runner"; function getTestExecutionService(): ITestExecutionService { const injector = new InjectorStub(); injector.register("testExecutionService", TestExecutionService); + injector.register("runController", {}); return injector.resolve("testExecutionService"); } diff --git a/test/stubs.ts b/test/stubs.ts index 28add17f06..947a60dd1b 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -9,6 +9,7 @@ import * as prompt from "inquirer"; import { Yok } from "./../lib/common/yok"; import { HostInfo } from "./../lib/common/host-info"; import { DevicePlatformsConstants } from "./../lib/common/mobile/device-platforms-constants"; +import { PrepareData } from "../lib/data/prepare-data"; export class LoggerStub implements ILogger { getLevel(): string { return undefined; } @@ -300,13 +301,16 @@ export class ProjectDataStub implements IProjectData { projectDir: string; projectName: string; get platformsDir(): string { - return this.plafromsDir || (this.projectDir && join(this.projectDir, "platforms")) || ""; + return this.platformsDirCache || (this.projectDir && join(this.projectDir, "platforms")) || ""; } set platformsDir(value) { - this.plafromsDir = value; + this.platformsDirCache = value; } projectFilePath: string; - projectIdentifiers: Mobile.IProjectIdentifier; + projectIdentifiers: Mobile.IProjectIdentifier = { + android: "org.nativescirpt.myiOSApp", + ios: "org.nativescript.myProjectApp" + }; projectId: string; dependencies: any; nsConfig: any; @@ -314,7 +318,7 @@ export class ProjectDataStub implements IProjectData { devDependencies: IStringDictionary; projectType: string; appResourcesDirectoryPath: string; - private plafromsDir: string = ""; + private platformsDirCache: string = ""; public androidManifestPath: string; public infoPlistPath: string; public appGradlePath: string; @@ -366,13 +370,18 @@ export class AndroidPluginBuildServiceStub implements IAndroidPluginBuildService } export class PlatformProjectServiceStub extends EventEmitter implements IPlatformProjectService { + constructor(private platform: string) { + super(); + } + getPlatformData(projectData: IProjectData): IPlatformData { return { - frameworkPackageName: "", - normalizedPlatformName: "", + frameworkPackageName: `tns-${this.platform.toLowerCase()}`, + normalizedPlatformName: this.platform.toLowerCase() === "ios" ? "iOS" : "Android", + platformNameLowerCase: this.platform.toLowerCase(), platformProjectService: this, projectRoot: "", - getBuildOutputPath: () => "", + getBuildOutputPath: (buildConfig: IBuildConfig) => "", getValidBuildOutputData: (buildOptions: IBuildOutputOptions) => ({ packageNames: [] }), frameworkFilesExtensions: [], appDestinationDirectoryPath: "", @@ -450,7 +459,7 @@ export class PlatformProjectServiceStub extends EventEmitter implements IPlatfor async cleanProject(projectRoot: string, projectData: IProjectData): Promise { return Promise.resolve(); } - async checkForChanges(changesInfo: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): Promise { + async checkForChanges(changesInfo: IProjectChangesInfo, options: any, projectData: IProjectData): Promise { // Nothing yet. } getFrameworkVersion(projectData: IProjectData): string { @@ -464,15 +473,16 @@ export class PlatformProjectServiceStub extends EventEmitter implements IPlatfor } } -export class PlatformsDataStub extends EventEmitter implements IPlatformsData { - public platformsNames: string[]; +export class NativeProjectDataStub extends EventEmitter implements IPlatformsDataService { + public platformNames: string[]; public getPlatformData(platform: string, projectData: IProjectData): IPlatformData { return { - frameworkPackageName: "", - platformProjectService: new PlatformProjectServiceStub(), + frameworkPackageName: `tns-${platform.toLowerCase()}`, + platformProjectService: new PlatformProjectServiceStub(platform), + platformNameLowerCase: platform.toLowerCase(), projectRoot: "", - normalizedPlatformName: "", + normalizedPlatformName: platform.toLowerCase() === "ios" ? "iOS" : "Android", appDestinationDirectoryPath: "", getBuildOutputPath: () => "", getValidBuildOutputData: (buildOptions: IBuildOutputOptions) => ({ packageNames: [] }), @@ -498,7 +508,18 @@ export class ProjectDataService implements IProjectDataService { removeDependency(dependencyName: string): void { } - getProjectData(projectDir: string): IProjectData { return null; } + getProjectData(projectDir: string): IProjectData { + return { + projectDir: "/path/to/my/projecDir", + projectName: "myTestProjectName", + platformsDir: "/path/to/my/projecDir/platforms", + projectIdentifiers: { + ios: "org.nativescript.myiosApp", + android: "org.nativescript.myAndroidApp" + }, + getAppResourcesRelativeDirectoryPath: () => "/path/to/my/projecDir/App_Resources" + }; + } async getAssetsStructure(opts: IProjectDir): Promise { return null; @@ -721,18 +742,18 @@ export class ChildProcessStub extends EventEmitter { } export class ProjectChangesService implements IProjectChangesService { - public async checkForChanges(checkForChangesOpts: ICheckForChangesOptions): Promise { + public async checkForChanges(platformData: IPlatformData, projectData: IProjectData, prepareData: PrepareData): Promise { return {}; } - public getPrepareInfo(platform: string): IPrepareInfo { + public getPrepareInfo(platformData: IPlatformData): IPrepareInfo { return null; } - public savePrepareInfo(platform: string): void { + public savePrepareInfo(platformData: IPlatformData): void { } - public getPrepareInfoFilePath(platform: string): string { + public getPrepareInfoFilePath(platformData: IPlatformData): string { return ""; } @@ -740,7 +761,7 @@ export class ProjectChangesService implements IProjectChangesService { return {}; } - public setNativePlatformStatus(platform: string, projectData: IProjectData, nativePlatformStatus: IAddedNativePlatform): void { + public setNativePlatformStatus(platformData: IPlatformData, addedPlatform: IAddedNativePlatform): void { return; } } @@ -764,119 +785,6 @@ export class CommandsService implements ICommandsService { } } -export class PlatformServiceStub extends EventEmitter implements IPlatformService { - public shouldPrepare(): Promise { - return Promise.resolve(true); - } - - public validateOptions(): Promise { - return Promise.resolve(true); - } - - public cleanPlatforms(platforms: string[]): Promise { - return Promise.resolve(); - } - - public addPlatforms(platforms: string[]): Promise { - return Promise.resolve(); - } - - public getInstalledPlatforms(): string[] { - return []; - } - - public getAvailablePlatforms(): string[] { - return []; - } - - public getPreparedPlatforms(): string[] { - return []; - } - - public saveBuildInfoFile(platform: string, projectDir: string, buildInfoFileDirname: string): void { - return; - } - - public async removePlatforms(platforms: string[]): Promise { - - } - - public updatePlatforms(platforms: string[]): Promise { - return Promise.resolve(); - } - - public preparePlatform(platformInfo: IPreparePlatformInfo): Promise { - return Promise.resolve(true); - } - - public shouldBuild(platform: string, projectData: IProjectData, buildConfig?: IBuildConfig): Promise { - return Promise.resolve(true); - } - - public buildPlatform(platform: string, buildConfig?: IBuildConfig): Promise { - return Promise.resolve(""); - } - - public async shouldInstall(device: Mobile.IDevice): Promise { - return true; - } - - public async validateInstall(device: Mobile.IDevice): Promise { - return; - } - - public installApplication(device: Mobile.IDevice, options: IRelease): Promise { - return Promise.resolve(); - } - - public deployPlatform(config: IDeployPlatformInfo): Promise { - return Promise.resolve(); - } - - public startApplication(platform: string, runOptions: IRunPlatformOptions): Promise { - return Promise.resolve(); - } - - public cleanDestinationApp(platformInfo: IPreparePlatformInfo): Promise { - return Promise.resolve(); - } - - public validatePlatformInstalled(platform: string): void { - - } - - public validatePlatform(platform: string): void { - - } - - isPlatformSupportedForOS(platform: string, projectData: IProjectData): boolean { - return true; - } - - public getLatestApplicationPackageForDevice(platformData: IPlatformData): IApplicationPackage { - return null; - } - - public getLatestApplicationPackageForEmulator(platformData: IPlatformData, buildConfig: IBuildConfig): IApplicationPackage { - return null; - } - - public copyLastOutput(platform: string, targetPath: string, buildConfig: IBuildConfig): void { - } - - public lastOutputPath(platform: string, buildConfig: IBuildConfig): string { - return ""; - } - - public readFile(device: Mobile.IDevice, deviceFilePath: string): Promise { - return Promise.resolve(""); - } - - public getCurrentPlatformVersion(platform: string, projectData: IProjectData): string { - return null; - } -} - export class AndroidResourcesMigrationServiceStub implements IAndroidResourcesMigrationService { canMigrate(platformString: string): boolean { return true; @@ -904,6 +812,35 @@ export class PerformanceService implements IPerformanceService { processExecutionData() { } } +export class PacoteServiceStub implements IPacoteService { + public async manifest(packageName: string, options?: IPacoteManifestOptions): Promise { + return ""; + } + public async extractPackage(packageName: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise { } +} + +class TerminalSpinnerStub { + public text: string; + public start(text?: string): ITerminalSpinner { return this; } + public stop(): ITerminalSpinner { return this; } + public succeed(text?: string): ITerminalSpinner { return this; } + public fail(text?: string): ITerminalSpinner { return this; } + public warn(text?: string): ITerminalSpinner { return this; } + public info(text?: string): ITerminalSpinner { return this; } + public clear(): ITerminalSpinner { return this; } + public render(): ITerminalSpinner { return this; } + public frame(): ITerminalSpinner { return this; } +} + +export class TerminalSpinnerServiceStub implements ITerminalSpinnerService { + public createSpinner(spinnerOptions?: ITerminalSpinnerOptions): ITerminalSpinner { + return new TerminalSpinnerStub(); + } + public async execute(spinnerOptions: ITerminalSpinnerOptions, action: () => Promise): Promise { + return null; + } +} + export class InjectorStub extends Yok implements IInjector { constructor() { super(); @@ -919,13 +856,12 @@ export class InjectorStub extends Yok implements IInjector { this.register('projectDataService', ProjectDataService); this.register('devicePlatformsConstants', DevicePlatformsConstants); this.register("androidResourcesMigrationService", AndroidResourcesMigrationServiceStub); - this.register("platformService", PlatformServiceStub); this.register("commandsService", CommandsService); this.register("projectChangesService", ProjectChangesService); this.register('childProcess', ChildProcessStub); this.register("liveSyncService", LiveSyncServiceStub); this.register("prompter", PrompterStub); - this.register('platformsData', PlatformsDataStub); + this.register('platformsDataService', NativeProjectDataStub); this.register("androidPluginBuildService", AndroidPluginBuildServiceStub); this.register('projectData', ProjectDataStub); this.register('packageInstallationManager', PackageInstallationManagerStub); @@ -942,5 +878,6 @@ export class InjectorStub extends Yok implements IInjector { getDevice: (): Mobile.IDevice => undefined, getDeviceByIdentifier: (): Mobile.IDevice => undefined }); + this.register("terminalSpinnerService", TerminalSpinnerServiceStub); } } diff --git a/test/tns-appstore-upload.ts b/test/tns-appstore-upload.ts index f6225139f9..053b784c1b 100644 --- a/test/tns-appstore-upload.ts +++ b/test/tns-appstore-upload.ts @@ -1,7 +1,10 @@ import { PublishIOS } from "../lib/commands/appstore-upload"; -import { PrompterStub, LoggerStub, ProjectDataStub } from "./stubs"; +import { PrompterStub, LoggerStub, ProjectDataStub, ProjectDataService } from "./stubs"; import * as chai from "chai"; import * as yok from "../lib/common/yok"; +import { PrepareNativePlatformService } from "../lib/services/platform/prepare-native-platform-service"; +import { BuildController } from "../lib/controllers/build-controller"; +import { IOSBuildData } from "../lib/data/build-data"; class AppStore { static itunesconnect = { @@ -15,16 +18,17 @@ class AppStore { options: any; prompter: PrompterStub; projectData: ProjectDataStub; - platformService: any; + buildController: BuildController; + prepareNativePlatformService: PrepareNativePlatformService; + platformCommandHelper: any; + platformValidationService: any; iOSPlatformData: any; iOSProjectService: any; - xcodebuildService: IXcodebuildService; loggerService: LoggerStub; itmsTransporterService: any; // Counters preparePlatformCalls: number = 0; - expectedPreparePlatformCalls: number = 0; archiveCalls: number = 0; expectedArchiveCalls: number = 0; exportArchiveCalls: number = 0; @@ -52,26 +56,24 @@ class AppStore { "devicePlatformsConstants": { "iOS": "iOS" }, - "platformService": this.platformService = {}, - "platformsData": { + "prepareNativePlatformService": this.prepareNativePlatformService = {}, + "platformCommandHelper": this.platformCommandHelper = {}, + "platformValidationService": this.platformValidationService = {}, + "buildController": this.buildController = { + buildPlatform: async () => { + this.archiveCalls++; + return "/Users/person/git/MyProject/platforms/ios/archive/MyProject.ipa"; + } + }, + "platformsDataService": { getPlatformData: (platform: string) => { chai.assert.equal(platform, "iOS"); return this.iOSPlatformData; } }, - "xcodebuildService": this.xcodebuildService = { - buildForDevice: async () => { - this.archiveCalls++; - return "/Users/person/git/MyProject/platforms/ios/archive/MyProject.ipa"; - }, - buildForSimulator: () => Promise.resolve(), - buildForAppStore: async () => { - this.archiveCalls++; - return "/Users/person/git/MyProject/platforms/ios/archive/MyProject.ipa"; - } - } } }); + this.projectData.initializeProjectData(this.iOSPlatformData.projectRoot); this.command = this.injector.resolveCommand("appstore"); } @@ -86,11 +88,12 @@ class AppStore { this.injector.register(serv, services.services[serv]); } } + + this.injector.register("projectDataService", ProjectDataService); } assert() { this.prompter.assert(); - chai.assert.equal(this.preparePlatformCalls, this.expectedPreparePlatformCalls, "Mismatched number of $platformService.preparePlatform calls."); chai.assert.equal(this.archiveCalls, this.expectedArchiveCalls, "Mismatched number of iOSProjectService.archive calls."); chai.assert.equal(this.itmsTransporterServiceUploadCalls, this.expectedItmsTransporterServiceUploadCalls, "Mismatched number of itmsTransporterService.upload calls."); } @@ -102,21 +105,13 @@ class AppStore { }); } - expectPreparePlatform() { - this.expectedPreparePlatformCalls = 1; - this.platformService.preparePlatform = (platformInfo: IPreparePlatformInfo) => { - chai.assert.equal(platformInfo.platform, "iOS"); - this.preparePlatformCalls++; - return Promise.resolve(true); - }; - } - expectArchive() { this.expectedArchiveCalls = 1; - this.xcodebuildService.buildForDevice = (platformData: any, projectData: IProjectData) => { + this.buildController.prepareAndBuild = (iOSBuildData: IOSBuildData) => { this.archiveCalls++; - chai.assert.equal(projectData.projectDir, "/Users/person/git/MyProject"); - return Promise.resolve("/Users/person/git/MyProject/platforms/ios/archive/MyProject.xcarchive"); + chai.assert.equal(iOSBuildData.projectDir, "/Users/person/git/MyProject"); + chai.assert.isTrue(iOSBuildData.buildForAppStore); + return Promise.resolve("/Users/person/git/MyProject/platforms/ios/archive/MyProject.ipa"); }; } @@ -134,7 +129,6 @@ class AppStore { async noArgs() { this.expectItunesPrompt(); - this.expectPreparePlatform(); this.expectArchive(); this.expectITMSTransporterUpload(); @@ -144,7 +138,6 @@ class AppStore { } async itunesconnectArgs() { - this.expectPreparePlatform(); this.expectArchive(); this.expectITMSTransporterUpload(); @@ -155,7 +148,6 @@ class AppStore { async teamIdOption() { this.expectItunesPrompt(); - this.expectPreparePlatform(); this.expectArchive(); this.expectITMSTransporterUpload(); diff --git a/test/update.ts b/test/update.ts index 80eb387f02..2577812e38 100644 --- a/test/update.ts +++ b/test/update.ts @@ -47,7 +47,7 @@ function createTestInjector( return "1.0.0"; } }); - testInjector.register("platformService", { + testInjector.register("platformCommandHelper", { getInstalledPlatforms: function(): string[] { return installedPlatforms; }, @@ -57,7 +57,8 @@ function createTestInjector( removePlatforms: async (): Promise => undefined, addPlatforms: async (): Promise => undefined, }); - testInjector.register("platformsData", { + testInjector.register("platformValidationService", {}); + testInjector.register("platformsDataService", { availablePlatforms: { Android: "Android", iOS: "iOS" @@ -93,15 +94,14 @@ describe("update command method tests", () => { validated = true; return Promise.resolve(); }); + const updateCommand = testInjector.resolve(UpdateCommand); - const canExecute = updateCommand.canExecute(["3.3.0"]); + await updateCommand.canExecute(["3.3.0"]); - return canExecute.then(() => { - assert.equal(validated, true); - }); + assert.equal(validated, true); }); - it("returns false if too many artuments", async () => { + it("returns false if too many arguments", async () => { const testInjector = createTestInjector([], ["android"]); const updateCommand = testInjector.resolve(UpdateCommand); const canExecuteOutput = await updateCommand.canExecute(["333", "111", "444"]); @@ -109,7 +109,7 @@ describe("update command method tests", () => { return assert.equal(canExecuteOutput.canExecute, false); }); - it("returns false if projectDir empty string", async () => { + it("returns false when projectDir is an empty string", async () => { const testInjector = createTestInjector([], ["android"], ""); const updateCommand = testInjector.resolve(UpdateCommand); const canExecuteOutput = await updateCommand.canExecute([]); @@ -117,7 +117,7 @@ describe("update command method tests", () => { return assert.equal(canExecuteOutput.canExecute, false); }); - it("returns true all ok", async () => { + it("returns true when the setup is correct", async () => { const testInjector = createTestInjector([], ["android"]); const updateCommand = testInjector.resolve(UpdateCommand); const canExecuteOutput = await updateCommand.canExecute(["3.3.0"]); @@ -142,17 +142,17 @@ describe("update command method tests", () => { const testInjector = createTestInjector(installedPlatforms); const fs = testInjector.resolve("fs"); const deleteDirectory: sinon.SinonStub = sandbox.stub(fs, "deleteDirectory"); - const platformService = testInjector.resolve("platformService"); + const platformCommandHelper = testInjector.resolve("platformCommandHelper"); sandbox.stub(fs, "copyFile").throws(); - sandbox.spy(platformService, "addPlatforms"); - sandbox.spy(platformService, "removePlatforms"); + sandbox.spy(platformCommandHelper, "addPlatforms"); + sandbox.spy(platformCommandHelper, "removePlatforms"); const updateCommand = testInjector.resolve(UpdateCommand); - return updateCommand.execute(["3.3.0"]).then(() => { - assert.isTrue(deleteDirectory.calledWith(path.join(projectFolder, UpdateCommand.tempFolder))); - assert.isFalse(platformService.removePlatforms.calledWith(installedPlatforms)); - assert.isFalse(platformService.addPlatforms.calledWith(installedPlatforms)); - }); + await updateCommand.execute(["3.3.0"]); + + assert.isTrue(deleteDirectory.calledWith(path.join(projectFolder, UpdateCommand.tempFolder))); + assert.isFalse(platformCommandHelper.removePlatforms.calledWith(installedPlatforms)); + assert.isFalse(platformCommandHelper.addPlatforms.calledWith(installedPlatforms)); }); it("calls copy to temp for package.json and folders(backup)", async () => { @@ -160,17 +160,18 @@ describe("update command method tests", () => { const fs = testInjector.resolve("fs"); const copyFileStub = sandbox.stub(fs, "copyFile"); const updateCommand = testInjector.resolve(UpdateCommand); - return updateCommand.execute(["3.3.0"]).then( () => { - assert.isTrue(copyFileStub.calledWith(path.join(projectFolder, "package.json"))); - for (const folder of UpdateCommand.folders) { - assert.isTrue(copyFileStub.calledWith(path.join(projectFolder, folder))); - } - }); + + await updateCommand.execute(["3.3.0"]); + + assert.isTrue(copyFileStub.calledWith(path.join(projectFolder, "package.json"))); + for (const folder of UpdateCommand.folders) { + assert.isTrue(copyFileStub.calledWith(path.join(projectFolder, folder))); + } }); it("calls copy from temp for package.json and folders to project folder(restore)", async () => { const testInjector = createTestInjector(); - testInjector.resolve("platformService").removePlatforms = () => { + testInjector.resolve("platformCommandHelper").removePlatforms = () => { throw new Error(); }; const fs = testInjector.resolve("fs"); @@ -179,13 +180,13 @@ describe("update command method tests", () => { const updateCommand = testInjector.resolve(UpdateCommand); const tempDir = path.join(projectFolder, UpdateCommand.tempFolder); - return updateCommand.execute(["3.3.0"]).then(() => { - assert.isTrue(copyFileStub.calledWith(path.join(tempDir, "package.json"), projectFolder)); - for (const folder of UpdateCommand.folders) { - assert.isTrue(deleteDirectoryStub.calledWith(path.join(projectFolder, folder))); - assert.isTrue(copyFileStub.calledWith(path.join(tempDir, folder), projectFolder)); - } - }); + await updateCommand.execute(["3.3.0"]); + + assert.isTrue(copyFileStub.calledWith(path.join(tempDir, "package.json"), projectFolder)); + for (const folder of UpdateCommand.folders) { + assert.isTrue(deleteDirectoryStub.calledWith(path.join(projectFolder, folder))); + assert.isTrue(copyFileStub.calledWith(path.join(tempDir, folder), projectFolder)); + } }); it("calls remove for all folders", async () => { @@ -193,37 +194,40 @@ describe("update command method tests", () => { const fs = testInjector.resolve("fs"); const deleteDirectory: sinon.SinonStub = sandbox.stub(fs, "deleteDirectory"); const updateCommand = testInjector.resolve(UpdateCommand); - return updateCommand.execute([]).then(() => { - for (const folder of UpdateCommand.folders) { - assert.isTrue(deleteDirectory.calledWith(path.join(projectFolder, folder))); - } - }); + + await updateCommand.execute([]); + + for (const folder of UpdateCommand.folders) { + assert.isTrue(deleteDirectory.calledWith(path.join(projectFolder, folder))); + } }); it("calls remove platforms and add platforms", async () => { const installedPlatforms: string[] = ["android"]; const testInjector = createTestInjector(installedPlatforms); - const platformService = testInjector.resolve("platformService"); - sandbox.spy(platformService, "addPlatforms"); - sandbox.spy(platformService, "removePlatforms"); + const platformCommandHelper = testInjector.resolve("platformCommandHelper"); + sandbox.spy(platformCommandHelper, "addPlatforms"); + sandbox.spy(platformCommandHelper, "removePlatforms"); const updateCommand = testInjector.resolve(UpdateCommand); - return updateCommand.execute([]).then(() => { - assert(platformService.removePlatforms.calledWith(installedPlatforms)); - assert(platformService.addPlatforms.calledWith(installedPlatforms)); - }); + + await updateCommand.execute([]); + + assert(platformCommandHelper.removePlatforms.calledWith(installedPlatforms)); + assert(platformCommandHelper.addPlatforms.calledWith(installedPlatforms)); }); it("call add platforms with specific verison", async () => { const version = "3.3.0"; const installedPlatforms: string[] = ["android"]; const testInjector = createTestInjector(installedPlatforms); - const platformService = testInjector.resolve("platformService"); - sandbox.spy(platformService, "addPlatforms"); - sandbox.spy(platformService, "removePlatforms"); + const platformCommandHelper = testInjector.resolve("platformCommandHelper"); + sandbox.spy(platformCommandHelper, "addPlatforms"); + sandbox.spy(platformCommandHelper, "removePlatforms"); + const updateCommand = testInjector.resolve(UpdateCommand); - return updateCommand.execute([version]).then(() => { - assert(platformService.addPlatforms.calledWith([`${installedPlatforms}@${version}`])); - }); + await updateCommand.execute([version]); + + assert(platformCommandHelper.addPlatforms.calledWith([`${installedPlatforms}@${version}`])); }); it("calls remove and add of core modules and widgets", async () => { @@ -239,12 +243,12 @@ describe("update command method tests", () => { }; const updateCommand = testInjector.resolve(UpdateCommand); - return updateCommand.execute([]).then(() => { - assert(pluginsService.add.calledWith("tns-core-modules")); - assert(pluginsService.remove.calledWith("tns-core-modules")); - assert(pluginsService.remove.calledWith("tns-core-modules-widgets")); - assert(pluginsService.ensureAllDependenciesAreInstalled.called); - }); + await updateCommand.execute([]); + + assert(pluginsService.add.calledWith("tns-core-modules")); + assert(pluginsService.remove.calledWith("tns-core-modules")); + assert(pluginsService.remove.calledWith("tns-core-modules-widgets")); + assert(pluginsService.ensureAllDependenciesAreInstalled.called); }); it("calls add of core modules with specific version", async () => { @@ -254,10 +258,11 @@ describe("update command method tests", () => { sandbox.spy(pluginsService, "remove"); sandbox.spy(pluginsService, "add"); sandbox.spy(pluginsService, "ensureAllDependenciesAreInstalled"); + const updateCommand = testInjector.resolve(UpdateCommand); - return updateCommand.execute([version]).then(() => { - assert(pluginsService.add.calledWith(`tns-core-modules@${version}`)); - }); + await updateCommand.execute([version]); + + assert(pluginsService.add.calledWith(`tns-core-modules@${version}`)); }); }); });