diff --git a/.travis.yml b/.travis.yml index 17da6134aa..f79e684e53 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ branches: - master - release - release-patch + - feature/webpack-only env: global: - DATE=$(date +%Y-%m-%d) diff --git a/.travis/add-publishConfig.js b/.travis/add-publishConfig.js index 7d03f227f1..b338e5e1aa 100644 --- a/.travis/add-publishConfig.js +++ b/.travis/add-publishConfig.js @@ -26,6 +26,9 @@ switch (branch) { case "master": packageDef.publishConfig.tag = "next"; break; + case "feature/webpack-only": + packageDef.publishConfig.tag = "webpack"; + break; default: throw new Error(`Unable to publish as the branch ${branch} does not have corresponding tag. Supported branches are master (next tag), release (rc tag) and release-patch (patch tag)`); } diff --git a/PublicAPI.md b/PublicAPI.md index 3fdbba0e30..d9faad77eb 100644 --- a/PublicAPI.md +++ b/PublicAPI.md @@ -666,11 +666,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/docs/man_pages/project/creation/init.md b/docs/man_pages/project/creation/init.md deleted file mode 100644 index bb5a0ebc1d..0000000000 --- a/docs/man_pages/project/creation/init.md +++ /dev/null @@ -1,25 +0,0 @@ -<% if (isJekyll) { %>--- -title: tns init -position: 2 ----<% } %> -# tns init - - -Usage | Synopsis ----|--- -General | `$ tns init [--path ] [--force]` - -Initializes a project for development. The command prompts you to provide your project configuration interactively and uses the information to create a new `package.json` file or update the existing one. - -### Options -* `--path` - Specifies the directory where you want to initialize the project, if different from the current directory. The directory must be empty. -* `--force` - If set, applies the default project configuration and does not show the interactive prompt. The default project configuration targets the latest official runtimes and sets `org.nativescript.` for application identifier. - -<% if(isHtml) { %> -### Related Commands - -Command | Description -----------|---------- -[create](create.html) | Creates a new project for native development with NativeScript from the default template or from an existing NativeScript project. -[install](/lib-management/install.html) | Installs all platforms and dependencies described in the `package.json` file in the current directory. -<% } %> \ No newline at end of file diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 3b77b4214b..ccc3349544 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -11,12 +11,20 @@ $injector.require("performanceService", "./services/performance-service"); $injector.requirePublic("projectService", "./services/project-service"); $injector.require("androidProjectService", "./services/android-project-service"); $injector.require("androidPluginBuildService", "./services/android-plugin-build-service"); +$injector.require("gradleCommandService", "./services/android/gradle-command-service"); +$injector.require("gradleBuildService", "./services/android/gradle-build-service"); +$injector.require("gradleBuildArgsService", "./services/android/gradle-build-args-service"); $injector.require("iOSEntitlementsService", "./services/ios-entitlements-service"); $injector.require("iOSExtensionsService", "./services/ios-extensions-service"); $injector.require("iOSWatchAppService", "./services/ios-watch-app-service"); $injector.require("iOSProjectService", "./services/ios-project-service"); $injector.require("iOSProvisionService", "./services/ios-provision-service"); $injector.require("xcconfigService", "./services/xcconfig-service"); +$injector.require("iOSSigningService", "./services/ios/ios-signing-service"); +$injector.require("xcodebuildArgsService", "./services/ios/xcodebuild-args-service"); +$injector.require("xcodebuildCommandService", "./services/ios/xcodebuild-command-service"); +$injector.require("xcodebuildService", "./services/ios/xcodebuild-service"); +$injector.require("exportOptionsPlistService", "./services/ios/export-options-plist-service"); $injector.require("cocoapodsService", "./services/cocoapods-service"); $injector.require("cocoaPodsPlatformManager", "./services/cocoapods-platform-manager"); @@ -25,13 +33,31 @@ $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("deviceInstallAppService", "./services/device/device-install-app-service"); + +$injector.require("platformController", "./controllers/platform-controller"); +$injector.require("prepareController", "./controllers/prepare-controller"); +$injector.require("deployController", "./controllers/deploy-controller"); +$injector.requirePublicClass("buildController", "./controllers/build-controller"); +$injector.requirePublicClass("runController", "./controllers/run-controller"); +$injector.requirePublicClass("debugController", "./controllers/debug-controller"); +$injector.requirePublicClass("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("liveSyncProcessDataService", "./services/livesync-process-data-service"); $injector.require("debugDataService", "./services/debug-data-service"); -$injector.requirePublicClass("debugService", "./services/debug-service"); $injector.require("iOSDeviceDebugService", "./services/ios-device-debug-service"); $injector.require("androidDeviceDebugService", "./services/android-device-debug-service"); @@ -40,8 +66,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"); @@ -59,8 +83,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"); @@ -109,9 +131,6 @@ $injector.require("xcprojService", "./services/xcproj-service"); $injector.require("versionsService", "./services/versions-service"); $injector.requireCommand("install", "./commands/install"); -$injector.require("projectInitService", "./services/project-init-service"); -$injector.requireCommand("init", "./commands/init"); - $injector.require("infoService", "./services/info-service"); $injector.requireCommand("info", "./commands/info"); @@ -127,17 +146,16 @@ $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"); $injector.require("iOSLiveSyncService", "./services/livesync/ios-livesync-service"); $injector.require("usbLiveSyncService", "./services/livesync/livesync-service"); // The name is used in https://github.com/NativeScript/nativescript-dev-typescript $injector.require("previewAppFilesService", "./services/livesync/playground/preview-app-files-service"); -$injector.require("previewAppLiveSyncService", "./services/livesync/playground/preview-app-livesync-service"); $injector.require("previewAppLogProvider", "./services/livesync/playground/preview-app-log-provider"); $injector.require("previewAppPluginsService", "./services/livesync/playground/preview-app-plugins-service"); $injector.require("previewSdkService", "./services/livesync/playground/preview-sdk-service"); @@ -191,11 +209,12 @@ $injector.require("hmrStatusService", "./services/hmr-status-service"); $injector.require("pacoteService", "./services/pacote-service"); $injector.require("qrCodeTerminalService", "./services/qr-code-terminal-service"); $injector.require("testInitializationService", "./services/test-initialization-service"); -$injector.require("workflowService", "./services/workflow-service"); $injector.require("networkConnectivityValidator", "./helpers/network-connectivity-validator"); $injector.requirePublic("cleanupService", "./services/cleanup-service"); +$injector.require("webpackCompilerService", "./services/webpack/webpack-compiler-service"); + $injector.require("applePortalSessionService", "./services/apple-portal/apple-portal-session-service"); $injector.require("applePortalCookieService", "./services/apple-portal/apple-portal-cookie-service"); $injector.require("applePortalApplicationService", "./services/apple-portal/apple-portal-application-service"); diff --git a/lib/commands/add-platform.ts b/lib/commands/add-platform.ts index 20d42e5ef1..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.$options.platformTemplate, 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 aadc7e665b..94acbabec3 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 0870e7b388..949c15c372 100644 --- a/lib/commands/appstore-upload.ts +++ b/lib/commands/appstore-upload.ts @@ -1,6 +1,7 @@ -import { StringCommandParameter } from "../common/command-params"; import * as path from "path"; -import { IOSProjectService } from "../services/ios-project-service"; +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), @@ -15,26 +16,18 @@ export class PublishIOS implements ICommand { private $prompter: IPrompter, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $hostInfo: IHostInfo, - private $errors: IErrors) { + private $errors: IErrors, + 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]; const mobileProvisionIdentifier = args[2]; const codeSignIdentity = args[3]; - const teamID = this.$options.teamId; let ipaFilePath = this.$options.ipa ? path.resolve(this.$options.ipa) : null; if (!username) { @@ -54,55 +47,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, - platformTemplate: this.$options.platformTemplate, - projectData: this.$projectData, - config: this.$options, - env: this.$options.env - }; - if (mobileProvisionIdentifier || codeSignIdentity) { - const iOSBuildConfig: 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 - }; - 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, iOSBuildConfig, this.$projectData); - ipaFilePath = this.$platformService.lastOutputPath(platform, iOSBuildConfig, 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); - const iOSProjectService = platformData.platformProjectService; - - const archivePath = await iOSProjectService.archive(this.$projectData); - this.$logger.info("Archive at: " + archivePath); + this.$logger.info("Building .ipa with the selected mobile provision and/or certificate."); - const exportPath = await iOSProjectService.exportArchive(this.$projectData, { archivePath, teamID, provision: mobileProvisionIdentifier || this.$options.provision }); - 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}`); } } @@ -120,7 +82,7 @@ export class PublishIOS implements ICommand { this.$errors.failWithoutHelp("iOS publishing is only available on Mac OS X."); } - 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 79ba67eeff..6acdb2c8f0 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -5,63 +5,32 @@ 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, - protected $logger: ILogger, - protected $workflowService: IWorkflowService) { - super($options, $platformsData, $platformService, $projectData); - this.$projectData.initializeProjectData(); + private $buildDataService: IBuildDataService, + protected $logger: ILogger) { + super($options, $platformsDataService, $platformValidationService, $projectData); + this.$projectData.initializeProjectData(); } + public dashedOptions = { + watch: { type: OptionType.Boolean, default: false, hasSensitiveValue: false }, + hmr: { type: OptionType.Boolean, default: false, hasSensitiveValue: false }, + }; + public async executeCore(args: string[]): Promise { - await this.$workflowService.handleLegacyWorkflow({ projectDir: this.$projectData.projectDir, settings: this.$options, skipWarnings: true }); 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, - platformTemplate: this.$options.platformTemplate, - 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`); } @@ -81,7 +50,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; } } @@ -92,17 +61,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, - $workflowService: IWorkflowService) { - super($options, $errors, $projectData, $platformsData, $devicePlatformsConstants, $platformService, $bundleValidatorHelper, $logger, $workflowService); + $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 { @@ -127,18 +97,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, - protected $logger: ILogger, - $workflowService: IWorkflowService) { - super($options, $errors, $projectData, $platformsData, $devicePlatformsConstants, $platformService, $bundleValidatorHelper, $logger, $workflowService); + $buildDataService: IBuildDataService, + protected $logger: ILogger) { + 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 e95daf8a9e..0000000000 --- a/lib/commands/clean-app.ts +++ /dev/null @@ -1,83 +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, - private $logger: ILogger) { - super($options, $platformsData, $platformService, $projectData); - this.$projectData.initializeProjectData(); - } - - public async execute(args: string[]): Promise { - this.$logger.warn(`"tns clean-app ${this.platform.toLowerCase()}" command has been deprecated and will be removed in the upcoming NativeScript CLI v6.0.0. More info can be found in this issue https://github.com/NativeScript/nativescript-cli/issues/4518.`); - - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { - bundle: !!this.$options.bundle, - release: this.$options.release, - useHotModuleReload: false - }; - const platformInfo: IPreparePlatformInfo = { - appFilesUpdaterOptions, - platform: this.platform.toLowerCase(), - config: this.$options, - platformTemplate: this.$options.platformTemplate, - 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, - $logger: ILogger) { - super($options, $projectData, $platformService, $errors, $devicePlatformsConstants, $platformsData, $logger); - } - - 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, - $logger: ILogger) { - super($options, $projectData, $platformService, $errors, $devicePlatformsConstants, $platformsData, $logger); - } - - 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 734b546b5b..30a2cfe5ab 100644 --- a/lib/commands/debug.ts +++ b/lib/commands/debug.ts @@ -7,24 +7,21 @@ export class DebugPlatformCommand extends ValidatePlatformCommandBase implements constructor(private platform: string, 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 $debugController: IDebugController, private $liveSyncCommandHelper: ILiveSyncCommandHelper, - private $androidBundleValidatorHelper: IAndroidBundleValidatorHelper, - private $workflowService: IWorkflowService) { - super($options, $platformsData, $platformService, $projectData); + private $androidBundleValidatorHelper: IAndroidBundleValidatorHelper) { + super($options, $platformsDataService, $platformValidationService, $projectData); } public async execute(args: string[]): Promise { - await this.$workflowService.handleLegacyWorkflow({ projectDir: this.$projectData.projectDir, settings: this.$options, skipWarnings: true }); await this.$devicesService.initialize({ platform: this.platform, deviceId: this.$options.device, @@ -32,18 +29,16 @@ export class DebugPlatformCommand extends ValidatePlatformCommandBase implements skipDeviceDetectionInterval: true }); - const debugOptions = _.cloneDeep(this.$options.argv); - const selectedDeviceForDebug = await this.$devicesService.pickSingleDevice({ onlyEmulators: this.$options.emulator, onlyDevices: this.$options.forDevice, deviceId: this.$options.device }); - 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)); + const debugOptions = _.cloneDeep(this.$options.argv); + const debugData = this.$debugDataService.getDebugData(selectedDeviceForDebug.deviceInfo.identifier, this.$projectData, debugOptions); + await this.$debugController.printDebugInformation(await this.$debugController.startDebug(debugData)); return; } @@ -51,7 +46,6 @@ export class DebugPlatformCommand extends ValidatePlatformCommandBase implements deviceDebugMap: { [selectedDeviceForDebug.deviceInfo.identifier]: true }, - // This will default in the liveSyncCommandHelper buildPlatform: undefined, skipNativePrepare: false }); @@ -60,7 +54,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`); } @@ -87,7 +81,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, @@ -110,7 +104,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 ad6fffc62b..62a35ee7b4 100644 --- a/lib/commands/deploy.ts +++ b/lib/commands/deploy.ts @@ -1,27 +1,31 @@ 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, + public dashedOptions = { + hmr: { type: OptionType.Boolean, default: false, hasSensitiveValue: false }, + }; + + 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); - this.$projectData.initializeProjectData(); + 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/init.ts b/lib/commands/init.ts deleted file mode 100644 index 85eb86a785..0000000000 --- a/lib/commands/init.ts +++ /dev/null @@ -1,14 +0,0 @@ -export class ProjectInitCommand implements ICommand { - public allowedParameters: ICommandParameter[] = []; - public enableHooks = false; - - constructor(private $logger: ILogger, - private $projectInitService: IProjectInitService) { } - - public async execute(args: string[]): Promise { - this.$logger.warn("This command is deprecated and it will be removed in the next major release of NativeScript"); - return this.$projectInitService.initialize(); - } -} - -$injector.registerCommand("init", ProjectInitCommand); diff --git a/lib/commands/install.ts b/lib/commands/install.ts index a92ee7a6ab..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.$options.platformTemplate, 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 bd450f52b9..f8ee41f87a 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.info("The project is prepared for: ", helpers.formatListOfNames(preparedPlatforms, "and")); } else { @@ -22,7 +22,7 @@ export class ListPlatformsCommand implements ICommand { this.$logger.info("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.info("Available platforms for this OS: ", formattedPlatformsList); this.$logger.info("No installed platforms found. Use $ tns platform add"); } diff --git a/lib/commands/platform-clean.ts b/lib/commands/platform-clean.ts index d7e6a3a1c4..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.$options.platformTemplate, 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 d0f821c808..71f6e6fdd3 100644 --- a/lib/commands/prepare.ts +++ b/lib/commands/prepare.ts @@ -1,40 +1,37 @@ 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 }, + hmr: { type: OptionType.Boolean, default: false, hasSensitiveValue: false }, + }; + constructor($options: IOptions, - $platformService: IPlatformService, + private $prepareController: PrepareController, + $platformValidationService: IPlatformValidationService, $projectData: IProjectData, private $platformCommandParameter: ICommandParameter, - $platformsData: IPlatformsData, - private $workflowService: IWorkflowService) { - super($options, $platformsData, $platformService, $projectData); - this.$projectData.initializeProjectData(); + $platformsDataService: IPlatformsDataService, + private $prepareDataService: PrepareDataService) { + super($options, $platformsDataService, $platformValidationService, $projectData); + this.$projectData.initializeProjectData(); } public async execute(args: string[]): Promise { - await this.$workflowService.handleLegacyWorkflow({ projectDir: this.$projectData.projectDir, settings: this.$options, skipWarnings: true }); - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { - bundle: !!this.$options.bundle, - release: this.$options.release, - useHotModuleReload: this.$options.hmr - }; - const platformInfo: IPreparePlatformInfo = { - platform: args[0], - appFilesUpdaterOptions, - platformTemplate: this.$options.platformTemplate, - 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 59cf02ad7e..a8bffb0bc4 100644 --- a/lib/commands/preview.ts +++ b/lib/commands/preview.ts @@ -7,29 +7,26 @@ export class PreviewCommand implements ICommand { constructor(private $analyticsService: IAnalyticsService, private $bundleValidatorHelper: IBundleValidatorHelper, private $errors: IErrors, - private $liveSyncService: ILiveSyncService, private $logger: ILogger, + private $previewAppController: IPreviewAppController, private $networkConnectivityValidator: INetworkConnectivityValidator, private $projectData: IProjectData, private $options: IOptions, private $previewAppLogProvider: IPreviewAppLogProvider, private $previewQrCodeService: IPreviewQrCodeService, - protected $workflowService: IWorkflowService, $cleanupService: ICleanupService) { this.$analyticsService.setShouldDispose(false); $cleanupService.setShouldDispose(false); } public async execute(): Promise { - await this.$workflowService.handleLegacyWorkflow({ projectDir: this.$projectData.projectDir, settings: this.$options, skipWarnings: true }); this.$previewAppLogProvider.on(DEVICE_LOG_EVENT_NAME, (deviceId: string, message: string) => { this.$logger.info(message); }); - await this.$liveSyncService.liveSyncToPreviewApp({ - bundle: !!this.$options.bundle, - useHotModuleReload: this.$options.hmr, + await this.$previewAppController.startPreview({ 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 cf83c86dd5..e8fcda7cdc 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -8,18 +8,16 @@ 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 $options: IOptions, - private $workflowService: IWorkflowService) { } + private $projectData: IProjectData + ) { } public allowedParameters: ICommandParameter[] = []; public async execute(args: string[]): Promise { - await this.$workflowService.handleLegacyWorkflow({ projectDir: this.$projectData.projectDir, settings: this.$options, skipWarnings: true }); await this.$analyticsService.trackPreviewAppData(this.platform, this.$projectData.projectDir); return this.$liveSyncCommandHelper.executeCommandLiveSync(this.platform, this.liveSyncCommandHelperAdditionalOptions); } @@ -38,12 +36,8 @@ export class RunCommandBase implements ICommand { this.platform = this.$devicePlatformsConstants.Android; } - const validatePlatformOutput = await this.$liveSyncCommandHelper.validatePlatform(this.platform); + await this.$liveSyncCommandHelper.validatePlatform(this.platform); - if (this.platform && validatePlatformOutput && validatePlatformOutput[this.platform.toLowerCase()]) { - const checkEnvironmentRequirementsOutput = validatePlatformOutput[this.platform.toLowerCase()].checkEnvironmentRequirementsOutput; - this.liveSyncCommandHelperAdditionalOptions.syncToPreviewApp = checkEnvironmentRequirementsOutput && checkEnvironmentRequirementsOutput.selectedOption === "Sync to Playground"; - } return true; } } @@ -64,13 +58,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 { @@ -80,11 +75,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; } } @@ -105,13 +100,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); @@ -120,7 +116,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`); } @@ -128,7 +124,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 51fd1efd64..abe1502d5f 100644 --- a/lib/commands/test.ts +++ b/lib/commands/test.ts @@ -1,8 +1,5 @@ -import * as helpers from "../common/helpers"; - abstract class TestCommandBase { public allowedParameters: ICommandParameter[] = []; - private projectFilesConfig: IProjectFilesConfig; protected abstract platform: string; protected abstract $projectData: IProjectData; protected abstract $testExecutionService: ITestExecutionService; @@ -11,18 +8,41 @@ abstract class TestCommandBase { protected abstract $platformEnvironmentRequirements: IPlatformEnvironmentRequirements; protected abstract $errors: IErrors; protected abstract $cleanupService: ICleanupService; - protected abstract $workflowService: IWorkflowService; + protected abstract $liveSyncCommandHelper: ILiveSyncCommandHelper; + protected abstract $devicesService: Mobile.IDevicesService; async execute(args: string[]): Promise { - await this.$workflowService.handleLegacyWorkflow({ projectDir: this.$projectData.projectDir, settings: this.$options, skipWarnings: true }); - 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.getLiveSyncData(this.$projectData.projectDir); + + 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, @@ -57,10 +77,10 @@ class TestAndroidCommand extends TestCommandBase implements ICommand { protected $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, protected $errors: IErrors, protected $cleanupService: ICleanupService, - protected $workflowService: IWorkflowService) { + protected $liveSyncCommandHelper: ILiveSyncCommandHelper, + protected $devicesService: Mobile.IDevicesService) { super(); } - } class TestIosCommand extends TestCommandBase implements ICommand { @@ -73,7 +93,8 @@ class TestIosCommand extends TestCommandBase implements ICommand { protected $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, protected $errors: IErrors, protected $cleanupService: ICleanupService, - protected $workflowService: IWorkflowService) { + protected $liveSyncCommandHelper: ILiveSyncCommandHelper, + protected $devicesService: Mobile.IDevicesService) { super(); } diff --git a/lib/commands/update-platform.ts b/lib/commands/update-platform.ts index df40f4f8a5..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.$options.platformTemplate, 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 bcdc250e2e..c60aef2b81 100644 --- a/lib/commands/update.ts +++ b/lib/commands/update.ts @@ -5,16 +5,17 @@ import { ValidatePlatformCommandBase } from "./command-base"; export class UpdateCommand extends ValidatePlatformCommandBase implements ICommand { public allowedParameters: ICommandParameter[] = []; - constructor($options: IOptions, - $projectData: IProjectData, - $platformService: IPlatformService, - $platformsData: IPlatformsData, - private $pluginsService: IPluginsService, - private $projectDataService: IProjectDataService, + constructor( private $fs: IFileSystem, private $logger: ILogger, - private $workflowService: IWorkflowService) { - super($options, $platformsData, $platformService, $projectData); + $options: IOptions, + private $platformCommandHelper: IPlatformCommandHelper, + $platformsDataService: IPlatformsDataService, + $platformValidationService: IPlatformValidationService, + private $pluginsService: IPluginsService, + $projectData: IProjectData, + private $projectDataService: IProjectDataService) { + super($options, $platformsDataService, $platformValidationService, $projectData); this.$projectData.initializeProjectData(); } @@ -29,11 +30,6 @@ export class UpdateCommand extends ValidatePlatformCommandBase implements IComma static readonly backupFailMessage: string = "Could not backup project folders!"; public async execute(args: string[]): Promise { - if (this.$options.workflow) { - await this.$workflowService.handleLegacyWorkflow({ projectDir: this.$projectData.projectDir, settings: this.$options, force: true }); - return; - } - const tmpDir = path.join(this.$projectData.projectDir, UpdateCommand.tempFolder); try { @@ -84,11 +80,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); @@ -100,12 +96,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.$options.platformTemplate, 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.$options.platformTemplate, 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); } @@ -113,12 +109,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 7959c0ad8f..815b5c1d72 100644 --- a/lib/common/declarations.d.ts +++ b/lib/common/declarations.d.ts @@ -1238,11 +1238,6 @@ interface IResourceLoader { readJson(path: string): any; } -interface IPluginVariablesHelper { - getPluginVariableFromVarOption(variableName: string, configuration?: string): any; - simplifyYargsObject(obj: any, configuration?: string): any; -} - /** * Used for getting strings for informational/error messages. */ @@ -1498,6 +1493,7 @@ interface IPromiseActions { interface IDeferPromise extends IPromiseActions { isRejected(): boolean; isPending(): boolean; + getResult(): any; promise: Promise; } diff --git a/lib/common/definitions/logger.d.ts b/lib/common/definitions/logger.d.ts index 6e098bc4c1..5d51828965 100644 --- a/lib/common/definitions/logger.d.ts +++ b/lib/common/definitions/logger.d.ts @@ -25,36 +25,7 @@ declare global { trace(formatStr?: any, ...args: any[]): void; printMarkdown(...args: any[]): void; prepare(item: any): string; - - /** - * DEPRECATED - * Do not use it. - */ - out(formatStr?: any, ...args: any[]): void; - - /** - * DEPRECATED - * Do not use it. - */ - write(...args: any[]): void; - - /** - * DEPRECATED - * Do not use it. - */ - printInfoMessageOnSameLine(message: string): void; - - /** - * DEPRECATED - * Do not use it. - */ - printMsgWithTimeout(message: string, timeout: number): Promise; - - /** - * DEPRECATED - * Do not use it. - */ - printOnStderr(formatStr?: any, ...args: any[]): void; + isVerbose(): boolean; } interface Log4JSAppenderConfiguration extends Configuration { diff --git a/lib/common/definitions/mobile.d.ts b/lib/common/definitions/mobile.d.ts index 1ce531beef..328f5467ab 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: ILiveSyncDeviceDescriptor[]): 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 5ca4bb5d3f..52b731e146 100644 --- a/lib/common/helpers.ts +++ b/lib/common/helpers.ts @@ -97,12 +97,6 @@ export function regExpEscape(input: string): string { return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } -export function isRecommendedAarFile(foundAarFile: string, packageJsonPluginName: string): boolean { - const filename = foundAarFile.replace(/^.*[\\\/]/, ''); - packageJsonPluginName = getShortPluginName(packageJsonPluginName); - return `${packageJsonPluginName}.aar` === filename; -} - export function getShortPluginName(pluginName: string): string { return sanitizePluginName(pluginName).replace(/[\-]/g, "_"); } @@ -136,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); }; @@ -157,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/logger/logger.ts b/lib/common/logger/logger.ts index 934ccdf496..bad1eb9442 100644 --- a/lib/common/logger/logger.ts +++ b/lib/common/logger/logger.ts @@ -135,6 +135,10 @@ export class Logger implements ILogger { this.info(formattedMessage, { [LoggerConfigData.skipNewLine]: true }); } + public isVerbose(): boolean { + return log4js.levels.DEBUG.isGreaterThanOrEqualTo(this.getLevel()); + } + private logMessage(inputData: any[], logMethod: string): void { this.initialize(); @@ -200,37 +204,6 @@ export class Logger implements ILogger { return argument; }); } - - /******************************************************************************************* - * Metods below are deprecated. Delete them in 6.0.0 release: * - * Present only for backwards compatibility as some plugins (nativescript-plugin-firebase) * - * use these methods in their hooks * - *******************************************************************************************/ - - out(...args: any[]): void { - this.info(...args); - } - - write(...args: any[]): void { - this.info(...args, { [LoggerConfigData.skipNewLine]: true }); - } - - printOnStderr(...args: string[]): void { - this.error(...args); - } - - printInfoMessageOnSameLine(message: string): void { - this.info(message, { [LoggerConfigData.skipNewLine]: true }); - } - - printMsgWithTimeout(message: string, timeout: number): Promise { - return new Promise((resolve, reject) => { - setTimeout(() => { - this.printInfoMessageOnSameLine(message); - resolve(); - }, timeout); - }); - } } $injector.register("logger", Logger); diff --git a/lib/common/mobile/mobile-core/android-process-service.ts b/lib/common/mobile/mobile-core/android-process-service.ts index ee6b4e5c88..f8a8f3397e 100644 --- a/lib/common/mobile/mobile-core/android-process-service.ts +++ b/lib/common/mobile/mobile-core/android-process-service.ts @@ -8,8 +8,10 @@ export class AndroidProcessService implements Mobile.IAndroidProcessService { private _forwardedLocalPorts: IDictionary; constructor(private $errors: IErrors, + private $cleanupService: ICleanupService, private $injector: IInjector, - private $net: INet) { + private $net: INet, + private $staticConfig: IStaticConfig) { this._devicesAdbs = {}; this._forwardedLocalPorts = {}; } @@ -120,8 +122,7 @@ export class AndroidProcessService implements Mobile.IAndroidProcessService { } this._forwardedLocalPorts[portForwardInputData.deviceIdentifier] = localPort; - // TODO: Uncomment for 6.0.0 release - // await this.$cleanupService.addCleanupCommand({ command: await this.$staticConfig.getAdbFilePath(), args: ["-s", portForwardInputData.deviceIdentifier, "forward", "--remove", `tcp:${localPort}`] }); + await this.$cleanupService.addCleanupCommand({ command: await this.$staticConfig.getAdbFilePath(), args: ["-s", portForwardInputData.deviceIdentifier, "forward", "--remove", `tcp:${localPort}`] }); return localPort && +localPort; } diff --git a/lib/common/mobile/mobile-core/devices-service.ts b/lib/common/mobile/mobile-core/devices-service.ts index 663d6f72c1..073aa73a1d 100644 --- a/lib/common/mobile/mobile-core/devices-service.ts +++ b/lib/common/mobile/mobile-core/devices-service.ts @@ -599,6 +599,16 @@ export class DevicesService extends EventEmitter implements Mobile.IDevicesServi } } + public getPlatformsFromDeviceDescriptors(deviceDescriptors: ILiveSyncDeviceDescriptor[]): 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/plugin-variables-helper.ts b/lib/common/plugin-variables-helper.ts deleted file mode 100644 index 45800e10f2..0000000000 --- a/lib/common/plugin-variables-helper.ts +++ /dev/null @@ -1,90 +0,0 @@ -export class PluginVariablesHelper implements IPluginVariablesHelper { - constructor(private $options: IOptions) { } - - /** - * Checks if the specified pluginVariable exists in the --var option specified by user. - * The variable can be added to --var option for configuration or globally, for ex.: - * `--var.APP_ID myAppIdentifier` or `--var.debug.APP_ID myAppIdentifier`. - * NOTE: If the variable is added for specific configuration and globally, - * the value for the specified configuration will be used as it has higher priority. For ex.: - * `--var.APP_ID myAppIdentifier1 --var.debug.APP_ID myAppIdentifier2` will return myAppIdentifier2 for debug configuration - * and myAppIdentifier for release configuration. - * @param {string} variableName The name of the plugin variable. - * @param {string} configuration The configuration for which the variable will be used. - * @returns {any} The value of the plugin variable specified in --var or undefined. - */ - public getPluginVariableFromVarOption(variableName: string, configuration?: string): any { - let varOption = this.$options.var; - configuration = configuration ? configuration.toLowerCase() : undefined; - const lowerCasedVariableName = variableName.toLowerCase(); - if (varOption) { - let configVariableValue: string; - let generalVariableValue: string; - if (variableName.indexOf(".") !== -1) { - varOption = this.simplifyYargsObject(varOption, configuration); - } - _.each(varOption, (propValue: any, propKey: string) => { - if (propKey.toLowerCase() === configuration) { - _.each(propValue, (configPropValue: string, configPropKey: string) => { - if (configPropKey.toLowerCase() === lowerCasedVariableName) { - configVariableValue = configPropValue; - return false; - } - }); - } else if (propKey.toLowerCase() === lowerCasedVariableName) { - generalVariableValue = propValue; - } - }); - - const value = configVariableValue || generalVariableValue; - if (value) { - const obj = Object.create(null); - obj[variableName] = value.toString(); - return obj; - } - } - - return undefined; - } - - /** - * Converts complicated yargs object with many subobjects, to simplified one. - * Use it when the plugin variable contains dots ("."). In this case yargs treats them as inner object instead of propery name. - * For ex. '--var.debug.DATA.APP.ID testId' will be converted to {debug: {DATA: {APP: {ID: testId}}}}, while we need {debug: {DATA.APP.ID: testId}} - * '--var.DATA.APP.ID testId' will be converted to DATA: {APP: {ID: testId}}}, while we need {DATA.APP.ID: testId} - * @param {any} obj varObject created by yargs - * @param {string} configuration The configuration for which the plugin variable will be used. - * @return {any} Converted object if the obj paramater is of type object, otherwise - the object itself. - */ - public simplifyYargsObject(obj: any, configuration?: string): any { - if (obj && typeof (obj) === "object") { - const convertedObject: any = Object.create({}); - - _.each(obj, (propValue: any, propKey: string) => { - if (typeof (propValue) !== "object") { - convertedObject[propKey] = propValue; - return false; - } - - configuration = configuration ? configuration.toLowerCase() : undefined; - const innerObj = this.simplifyYargsObject(propValue, configuration); - - if (propKey.toLowerCase() === configuration) { - // for --var.debug.DATA.APP.ID testId - convertedObject[propKey] = innerObj; - } else { - // for --var.DATA.APP.ID testId - _.each(innerObj, (innerPropValue: any, innerPropKey: string) => { - convertedObject[`${propKey}.${innerPropKey}`] = innerPropValue; - }); - } - - }); - - return convertedObject; - } - - return obj; - } -} -$injector.register("pluginVariablesHelper", PluginVariablesHelper); diff --git a/lib/common/services/commands-service.ts b/lib/common/services/commands-service.ts index f46e63e4b1..b4bb6566d0 100644 --- a/lib/common/services/commands-service.ts +++ b/lib/common/services/commands-service.ts @@ -115,7 +115,6 @@ export class CommandsService implements ICommandsService { const dashedOptions = command ? command.dashedOptions : null; this.$options.validateOptions(dashedOptions, projectData); - this.$options.printMessagesForDeprecatedOptions(this.$logger); } return this.canExecuteCommand(commandName, commandArguments); 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/test/unit-tests/stubs.ts b/lib/common/test/unit-tests/stubs.ts index 075dcb4806..199980b088 100644 --- a/lib/common/test/unit-tests/stubs.ts +++ b/lib/common/test/unit-tests/stubs.ts @@ -51,6 +51,7 @@ export class CommonLoggerStub implements ILogger { printInfoMessageOnSameLine(message: string): void { } async printMsgWithTimeout(message: string, timeout: number): Promise { } printOnStderr(formatStr?: any, ...args: any[]): void { } + isVerbose(): boolean { return false; } } export class ErrorsStub implements IErrors { 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 e70a951c17..83aa5e3fbb 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -113,6 +113,9 @@ class ItunesConnectApplicationTypesClass implements IiTunesConnectApplicationTyp public Mac = "Mac OS X App"; } +export const iOSAppResourcesFolderName = "iOS"; +export const androidAppResourcesFolderName = "Android"; + export const ItunesConnectApplicationTypes = new ItunesConnectApplicationTypesClass(); export const VUE_NAME = "vue"; export const ANGULAR_NAME = "angular"; @@ -139,6 +142,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,14 +282,14 @@ 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" }; export enum IOSDeviceTargets { diff --git a/lib/controllers/build-controller.ts b/lib/controllers/build-controller.ts new file mode 100644 index 0000000000..a5e0bbf2f2 --- /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.info("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); + } + + const handler = (data: any) => { + this.emit(constants.BUILD_OUTPUT_EVENT_NAME, data); + this.$logger.info(data.data.toString(), { [constants.LoggerConfigData.skipNewLine]: true }); + }; + + await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME, platformData.platformProjectService, handler, platformData.platformProjectService.buildProject(platformData.projectRoot, projectData, buildData)); + + const buildInfoFileDir = platformData.getBuildOutputPath(buildData); + this.$buildInfoFileService.saveLocalBuildInfo(platformData, buildInfoFileDir); + + this.$logger.info("Project successfully built."); + + const result = await this.$buildArtefactsService.getLatestAppPackagePath(platformData, buildData); + + if (buildData.copyTo) { + this.$buildArtefactsService.copyLatestAppPackage(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.getAllAppPackages(outputPath, validBuildOutputData); + if (packages.length === 0) { + return true; + } + + const prepareInfo = this.$projectChangesService.getPrepareInfo(platformData); + const buildInfo = this.$buildInfoFileService.getLocalBuildInfo(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/debug-controller.ts b/lib/controllers/debug-controller.ts new file mode 100644 index 0000000000..6b079294d8 --- /dev/null +++ b/lib/controllers/debug-controller.ts @@ -0,0 +1,228 @@ +import { performanceLog } from "../common/decorators"; +import { EOL } from "os"; +import { parse } from "url"; +import { CONNECTED_STATUS } from "../common/constants"; +import { TrackActionNames, DebugCommandErrors, CONNECTION_ERROR_EVENT_NAME, DebugTools, DEBUGGER_DETACHED_EVENT_NAME, DEBUGGER_ATTACHED_EVENT_NAME } from "../constants"; +import { EventEmitter } from "events"; + +export class DebugController extends EventEmitter implements IDebugController { + private _platformDebugServices: IDictionary = {}; + + constructor( + private $analyticsService: IAnalyticsService, + private $debugDataService: IDebugDataService, + private $devicesService: Mobile.IDevicesService, + private $errors: IErrors, + private $injector: IInjector, + private $liveSyncProcessDataService: ILiveSyncProcessDataService, + private $logger: ILogger, + private $mobileHelper: Mobile.IMobileHelper, + private $projectDataService: IProjectDataService + ) { + super(); + } + + @performanceLog() + public async startDebug(debugData: IDebugData): Promise { + const { debugOptions: options } = debugData; + const device = this.$devicesService.getDeviceByIdentifier(debugData.deviceIdentifier); + + if (!device) { + this.$errors.failWithoutHelp(`Cannot find device with identifier ${debugData.deviceIdentifier}.`); + } + + if (device.deviceInfo.status !== CONNECTED_STATUS) { + this.$errors.failWithoutHelp(`The device with identifier ${debugData.deviceIdentifier} is unreachable. Make sure it is Trusted and try again.`); + } + + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: TrackActionNames.Debug, + device, + additionalData: this.$mobileHelper.isiOSPlatform(device.deviceInfo.platform) && options && options.inspector ? DebugTools.Inspector : DebugTools.Chrome, + projectDir: debugData.projectDir + }); + + if (!(await device.applicationManager.isApplicationInstalled(debugData.applicationIdentifier))) { + this.$errors.failWithoutHelp(`The application ${debugData.applicationIdentifier} is not installed on device with identifier ${debugData.deviceIdentifier}.`); + } + + const debugService = this.getDeviceDebugService(device); + if (!debugService) { + this.$errors.failWithoutHelp(`Unsupported device OS: ${device.deviceInfo.platform}. You can debug your applications only on iOS or Android.`); + } + + const debugOptions: IDebugOptions = _.cloneDeep(options); + const debugResultInfo = await debugService.debug(debugData, debugOptions); + + return this.getDebugInformation(debugResultInfo, device.deviceInfo.identifier); + } + + public enableDebugging(enableDebuggingData: IEnableDebuggingData): Promise[] { + const { deviceIdentifiers } = enableDebuggingData; + + return _.map(deviceIdentifiers, deviceIdentifier => this.enableDebuggingCore(enableDebuggingData.projectDir, deviceIdentifier, enableDebuggingData.debugOptions)); + } + + public async disableDebugging(disableDebuggingData: IDisableDebuggingData): Promise { + const { deviceIdentifiers, projectDir } = disableDebuggingData; + + for (const deviceIdentifier of deviceIdentifiers) { + const liveSyncProcessInfo = this.$liveSyncProcessDataService.getPersistedData(projectDir); + if (liveSyncProcessInfo.currentSyncAction) { + await liveSyncProcessInfo.currentSyncAction; + } + + const currentDeviceDescriptor = this.getDeviceDescriptor(projectDir, deviceIdentifier); + + if (currentDeviceDescriptor) { + currentDeviceDescriptor.debuggingEnabled = false; + } else { + this.$errors.failWithoutHelp(`Couldn't disable debugging for ${deviceIdentifier}`); + } + + const currentDevice = this.$devicesService.getDeviceByIdentifier(currentDeviceDescriptor.identifier); + if (!currentDevice) { + this.$errors.failWithoutHelp(`Couldn't disable debugging for ${deviceIdentifier}. Could not find device.`); + } + + await this.stopDebug(currentDevice.deviceInfo.identifier); + + this.emit(DEBUGGER_DETACHED_EVENT_NAME, { deviceIdentifier }); + } + } + + public async attachDebugger(attachDebuggerData: IAttachDebuggerData): Promise { + // Default values + if (attachDebuggerData.debugOptions) { + attachDebuggerData.debugOptions.chrome = attachDebuggerData.debugOptions.chrome === undefined ? true : attachDebuggerData.debugOptions.chrome; + attachDebuggerData.debugOptions.start = attachDebuggerData.debugOptions.start === undefined ? true : attachDebuggerData.debugOptions.start; + } else { + attachDebuggerData.debugOptions = { + chrome: true, + start: true + }; + } + + const projectData = this.$projectDataService.getProjectData(attachDebuggerData.projectDir); + const debugData = this.$debugDataService.getDebugData(attachDebuggerData.deviceIdentifier, projectData, attachDebuggerData.debugOptions); + // 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.startDebug(debugData); + const result = this.printDebugInformation(debugInfo, attachDebuggerData.debugOptions.forceDebuggerAttachedEvent); + return result; + } + + @performanceLog() + public async enableDebuggingCoreWithoutWaitingCurrentAction(projectDir: string, deviceIdentifier: string, debugOptions: IDebugOptions): Promise { + const deviceDescriptor = this.getDeviceDescriptor(projectDir, deviceIdentifier); + if (!deviceDescriptor) { + this.$errors.failWithoutHelp(`Couldn't enable debugging for ${deviceIdentifier}`); + } + + deviceDescriptor.debuggingEnabled = true; + deviceDescriptor.debugOptions = debugOptions; + + const currentDeviceInstance = this.$devicesService.getDeviceByIdentifier(deviceIdentifier); + const attachDebuggerData: IAttachDebuggerData = { + deviceIdentifier, + isEmulator: currentDeviceInstance.isEmulator, + outputPath: deviceDescriptor.buildData.outputPath, + platform: currentDeviceInstance.deviceInfo.platform, + projectDir, + debugOptions + }; + + let debugInformation: IDebugInformation; + try { + debugInformation = await this.attachDebugger(attachDebuggerData); + } catch (err) { + this.$logger.trace("Couldn't attach debugger, will modify options and try again.", err); + attachDebuggerData.debugOptions.start = false; + try { + debugInformation = await this.attachDebugger(attachDebuggerData); + } catch (innerErr) { + this.$logger.trace("Couldn't attach debugger with modified options.", innerErr); + throw err; + } + } + + return debugInformation; + } + + 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 async stopDebug(deviceIdentifier: string): Promise { + const device = this.$devicesService.getDeviceByIdentifier(deviceIdentifier); + const debugService = this.getDeviceDebugService(device); + await debugService.debugStop(); + } + + private getDeviceDescriptor(projectDir: string, deviceIdentifier: string): ILiveSyncDeviceDescriptor { + const deviceDescriptors = this.$liveSyncProcessDataService.getDeviceDescriptors(projectDir); + const currentDeviceDescriptor = _.find(deviceDescriptors, d => d.identifier === deviceIdentifier); + + return currentDeviceDescriptor; + } + + private getDeviceDebugService(device: Mobile.IDevice): IDeviceDebugService { + if (!this._platformDebugServices[device.deviceInfo.identifier]) { + const devicePlatform = device.deviceInfo.platform; + if (this.$mobileHelper.isiOSPlatform(devicePlatform)) { + this._platformDebugServices[device.deviceInfo.identifier] = this.$injector.resolve("iOSDeviceDebugService", { device }); + } else if (this.$mobileHelper.isAndroidPlatform(devicePlatform)) { + this._platformDebugServices[device.deviceInfo.identifier] = this.$injector.resolve("androidDeviceDebugService", { device }); + } else { + this.$errors.failWithoutHelp(DebugCommandErrors.UNSUPPORTED_DEVICE_OS_FOR_DEBUGGING); + } + + this.attachConnectionErrorHandlers(this._platformDebugServices[device.deviceInfo.identifier]); + } + + return this._platformDebugServices[device.deviceInfo.identifier]; + } + + private attachConnectionErrorHandlers(platformDebugService: IDeviceDebugService) { + let connectionErrorHandler = (e: Error) => this.emit(CONNECTION_ERROR_EVENT_NAME, e); + connectionErrorHandler = connectionErrorHandler.bind(this); + platformDebugService.on(CONNECTION_ERROR_EVENT_NAME, connectionErrorHandler); + } + + private getDebugInformation(debugResultInfo: IDebugResultInfo, deviceIdentifier: string): IDebugInformation { + const debugInfo: IDebugInformation = { + url: debugResultInfo.debugUrl, + port: 0, + deviceIdentifier + }; + + if (debugResultInfo.debugUrl) { + const parseQueryString = true; + const wsQueryParam = parse(debugResultInfo.debugUrl, parseQueryString).query.ws; + const hostPortSplit = wsQueryParam && wsQueryParam.split(":"); + debugInfo.port = hostPortSplit && +hostPortSplit[1]; + } + + return debugInfo; + } + + private async enableDebuggingCore(projectDir: string, deviceIdentifier: string, debugOptions: IDebugOptions): Promise { + const liveSyncProcessInfo = this.$liveSyncProcessDataService.getPersistedData(projectDir); + if (liveSyncProcessInfo && liveSyncProcessInfo.currentSyncAction) { + await liveSyncProcessInfo.currentSyncAction; + } + + return this.enableDebuggingCoreWithoutWaitingCurrentAction(projectDir, deviceIdentifier, debugOptions); + } +} +$injector.register("debugController", DebugController); diff --git a/lib/controllers/deploy-controller.ts b/lib/controllers/deploy-controller.ts new file mode 100644 index 0000000000..1574926ea4 --- /dev/null +++ b/lib/controllers/deploy-controller.ts @@ -0,0 +1,20 @@ +export class DeployController { + + constructor( + private $buildController: IBuildController, + private $deviceInstallAppService: IDeviceInstallAppService, + private $devicesService: Mobile.IDevicesService + ) { } + + public async deploy(data: IDeployData): Promise { + const { buildData, deviceDescriptors } = data; + + const executeAction = async (device: Mobile.IDevice) => { + const packageFilePath = await this.$buildController.prepareAndBuild({ ...buildData, buildForDevice: !device.isEmulator }); + await this.$deviceInstallAppService.installOnDevice(device, { ...buildData, buildForDevice: !device.isEmulator }, packageFilePath); + }; + + 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..b8dbb690c6 --- /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.info("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.info(`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..4fe1c87582 --- /dev/null +++ b/lib/controllers/prepare-controller.ts @@ -0,0 +1,166 @@ +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, PACKAGE_JSON_FILE_NAME, PLATFORMS_DIR_NAME } 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 $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder, + 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.info("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, prepareData); + const hasNativeChanges = await this.$prepareNativePlatformService.prepareNativePlatform(platformData, projectData, prepareData); + result = { hasNativeChanges, platform: prepareData.platform.toLowerCase() }; + } + + await this.$projectChangesService.savePrepareInfo(platformData, projectData, prepareData); + + this.$logger.info(`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, prepareData); // -> 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, prepareData: IPrepareData): 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, prepareData); + 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 = await this.getWatcherPatterns(platformData, projectData); + + 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.trace(`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; + } + + @hook('watchPatterns') + public async getWatcherPatterns(platformData: IPlatformData, projectData: IProjectData): Promise { + const pluginsNativeDirectories = this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir) + .filter(dep => dep.nativescript) + .map(dep => path.join(dep.directory, PLATFORMS_DIR_NAME, platformData.platformNameLowerCase)); + + const patterns = [ + path.join(projectData.projectDir, PACKAGE_JSON_FILE_NAME), + path.join(projectData.getAppDirectoryPath(), PACKAGE_JSON_FILE_NAME), + path.join(projectData.getAppResourcesRelativeDirectoryPath(), platformData.normalizedPlatformName), + ].concat(pluginsNativeDirectories); + + return patterns; + } + + 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..9cf26564d8 --- /dev/null +++ b/lib/controllers/preview-app-controller.ts @@ -0,0 +1,157 @@ +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 { PrepareDataService } from "../services/prepare-data-service"; +import { PreviewAppLiveSyncEvents } from "../services/livesync/playground/preview-app-constants"; + +export class PreviewAppController extends EventEmitter implements IPreviewAppController { + private deviceInitializationPromise: IDictionary> = {}; + private promise = Promise.resolve(); + + constructor( + private $analyticsService: IAnalyticsService, + private $errors: IErrors, + private $hmrStatusService: IHmrStatusService, + private $logger: ILogger, + public $hooksService: IHooksService, + private $prepareController: PrepareController, + private $previewAppFilesService: IPreviewAppFilesService, + private $previewAppPluginsService: IPreviewAppPluginsService, + private $previewDevicesService: IPreviewDevicesService, + private $previewQrCodeService: IPreviewQrCodeService, + private $previewSdkService: IPreviewSdkService, + private $prepareDataService: PrepareDataService + ) { super(); } + + public async startPreview(data: IPreviewAppLiveSyncData): Promise { + await this.previewCore(data); + + const url = this.$previewSdkService.getQrCodeUrl({ projectDir: data.projectDir, useHotModuleReload: data.useHotModuleReload }); + const result = await this.$previewQrCodeService.getLiveSyncQrCode(url); + + return result; + } + + public async stopPreview(): Promise { + this.$previewSdkService.stop(); + this.$previewDevicesService.updateConnectedDevices([]); + } + + private async previewCore(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 + }); + } + + await this.$hooksService.executeBeforeHooks("preview-sync", { ...data, platform: device.platform }); + + 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, nativePrepare: { skipNativePrepare: true }, watch: 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.emit(PreviewAppLiveSyncEvents.PREVIEW_APP_LIVE_SYNC_ERROR, { + error, + data, + platform: device.platform, + deviceId: device.id + }); + } + }); + return null; + } + + @performanceLog() + private async handlePrepareReadyEvent(data: IPreviewAppLiveSyncData, hmrData: IPlatformHmrData, files: string[], platform: string) { + await this.promise + .then(async () => { + const platformHmrData = _.cloneDeep(hmrData); + + this.promise = this.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.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)}`); + } + } + + private 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) { + this.$logger.info(`Start syncing changes for platform ${platform}.`); + await this.$previewSdkService.applyChanges(payloads); + this.$logger.info(`Successfully synced ${payloads.files.map(filePayload => filePayload.file.yellow)} for platform ${platform}.`); + } + } 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, + deviceId + }); + } + } +} +$injector.register("previewAppController", PreviewAppController); diff --git a/lib/controllers/run-controller.ts b/lib/controllers/run-controller.ts new file mode 100644 index 0000000000..df5ec94d47 --- /dev/null +++ b/lib/controllers/run-controller.ts @@ -0,0 +1,435 @@ +import { HmrConstants, DeviceDiscoveryEventNames } from "../common/constants"; +import { PREPARE_READY_EVENT_NAME, TrackActionNames, DEBUGGER_DETACHED_EVENT_NAME, RunOnDeviceEvents, USER_INTERACTION_NEEDED_EVENT_NAME } from "../constants"; +import { cache, performanceLog } from "../common/decorators"; +import { EventEmitter } from "events"; + +export class RunController extends EventEmitter implements IRunController { + private rebuiltInformation: IDictionary<{ packageFilePath: string, platform: string, isEmulator: boolean }> = { }; + + constructor( + protected $analyticsService: IAnalyticsService, + private $buildController: IBuildController, + private $debugController: IDebugController, + private $deviceInstallAppService: IDeviceInstallAppService, + protected $devicesService: Mobile.IDevicesService, + protected $errors: IErrors, + protected $injector: IInjector, + private $hmrStatusService: IHmrStatusService, + public $hooksService: IHooksService, + private $liveSyncServiceResolver: ILiveSyncServiceResolver, + private $liveSyncProcessDataService: ILiveSyncProcessDataService, + protected $logger: ILogger, + protected $mobileHelper: Mobile.IMobileHelper, + private $platformsDataService: IPlatformsDataService, + private $pluginsService: IPluginsService, + private $prepareController: IPrepareController, + private $prepareDataService: IPrepareDataService, + private $prepareNativePlatformService: IPrepareNativePlatformService, + protected $projectDataService: IProjectDataService + ) { + super(); + } + + public async run(runData: IRunData): Promise { + const { liveSyncInfo, deviceDescriptors } = runData; + const { projectDir } = liveSyncInfo; + + const projectData = this.$projectDataService.getProjectData(projectDir); + await this.initializeSetup(projectData); + + const platforms = this.$devicesService.getPlatformsFromDeviceDescriptors(deviceDescriptors); + const deviceDescriptorsForInitialSync = this.getDeviceDescriptorsForInitialSync(projectDir, deviceDescriptors); + + this.$liveSyncProcessDataService.persistData(projectDir, deviceDescriptors, platforms); + + const shouldStartWatcher = !liveSyncInfo.skipWatcher && this.$liveSyncProcessDataService.hasDeviceDescriptors(projectDir); + 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(data: IStopRunData): Promise { + const { projectDir, deviceIdentifiers, stopOptions } = data; + const liveSyncProcessInfo = this.$liveSyncProcessDataService.getPersistedData(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.emitCore(RunOnDeviceEvents.runOnDeviceStopped, { + projectDir, + deviceIdentifier + }); + }); + } + } + + public getDeviceDescriptors(data: { projectDir: string }): ILiveSyncDeviceDescriptor[] { + return this.$liveSyncProcessDataService.getDeviceDescriptors(data.projectDir); + } + + protected async refreshApplication(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, filesChangeEventData: IFilesChangeEventData, deviceDescriptor: ILiveSyncDeviceDescriptor, settings?: IRefreshApplicationSettings): Promise { + const result = deviceDescriptor.debuggingEnabled ? + await this.refreshApplicationWithDebug(projectData, liveSyncResultInfo, filesChangeEventData, deviceDescriptor, settings) : + await this.refreshApplicationWithoutDebug(projectData, liveSyncResultInfo, filesChangeEventData, deviceDescriptor, settings); + + return result; + } + + protected async refreshApplicationWithDebug(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, filesChangeEventData: IFilesChangeEventData, deviceDescriptor: ILiveSyncDeviceDescriptor, settings?: IRefreshApplicationSettings): Promise { + const debugOptions = deviceDescriptor.debugOptions || {}; + + liveSyncResultInfo.waitForDebugger = !!debugOptions.debugBrk; + + const refreshInfo = await this.refreshApplicationWithoutDebug(projectData, liveSyncResultInfo, filesChangeEventData, deviceDescriptor, settings); + + // 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; + + await this.$debugController.enableDebuggingCoreWithoutWaitingCurrentAction(projectData.projectDir, deviceDescriptor.identifier, debugOptions); + + return refreshInfo; + } + + @performanceLog() + protected async refreshApplicationWithoutDebug(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, filesChangeEventData: IFilesChangeEventData, deviceDescriptor: ILiveSyncDeviceDescriptor, settings?: IRefreshApplicationSettings): Promise { + const result = { didRestart: false }; + const platform = liveSyncResultInfo.deviceAppData.platform; + const applicationIdentifier = projectData.projectIdentifiers[platform.toLowerCase()]; + const platformLiveSyncService = this.$liveSyncServiceResolver.resolveLiveSyncService(platform); + + try { + let shouldRestart = filesChangeEventData && filesChangeEventData.hasNativeChanges; + if (!shouldRestart) { + shouldRestart = await platformLiveSyncService.shouldRestart(projectData, liveSyncResultInfo); + } + + if (!shouldRestart) { + shouldRestart = !await platformLiveSyncService.tryRefreshApplication(projectData, liveSyncResultInfo); + } + + if (shouldRestart) { + this.emit(DEBUGGER_DETACHED_EVENT_NAME, { deviceIdentifier: liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier }); + 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); + + const device = liveSyncResultInfo.deviceAppData.device; + const deviceIdentifier = device.deviceInfo.identifier; + + if (!settings || !settings.shouldSkipEmitLiveSyncNotification) { + this.emitCore(RunOnDeviceEvents.runOnDeviceNotification, { + projectDir: projectData.projectDir, + deviceIdentifier: device.deviceInfo.identifier, + applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()], + notification: msg + }); + } + + if (settings && settings.shouldCheckDeveloperDiscImage && (err.message || err) === "Could not find developer disk image") { + const attachDebuggerOptions: IAttachDebuggerData = { + platform: device.deviceInfo.platform, + isEmulator: device.isEmulator, + projectDir: projectData.projectDir, + deviceIdentifier, + debugOptions: deviceDescriptor.debugOptions, + outputPath: deviceDescriptor.buildData.outputPath + }; + this.emit(USER_INTERACTION_NEEDED_EVENT_NAME, attachDebuggerOptions); + } + } + + return result; + } + + private getDeviceDescriptorsForInitialSync(projectDir: string, deviceDescriptors: ILiveSyncDeviceDescriptor[]) { + const currentRunData = this.$liveSyncProcessDataService.getPersistedData(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.$liveSyncProcessDataService.getAllPersistedData()) { + try { + const deviceDescriptors = this.getDeviceDescriptors({ projectDir }); + if (_.find(deviceDescriptors, d => d.identifier === device.deviceInfo.identifier)) { + await this.stop({ projectDir, deviceIdentifiers: [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: ILiveSyncDeviceDescriptor[]): Promise { + this.rebuiltInformation = {}; + + 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, + ...deviceDescriptor.buildData, + nativePrepare: { skipNativePrepare: !!deviceDescriptor.skipNativePrepare }, + watch: !liveSyncInfo.skipWatcher, + }); + + const prepareResultData = await this.$prepareController.prepare(prepareData); + const buildData = { ...deviceDescriptor.buildData, buildForDevice: !device.isEmulator }; + + try { + let packageFilePath: string = null; + + // 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. + if (this.rebuiltInformation[platformData.platformNameLowerCase] && (this.$mobileHelper.isAndroidPlatform(platformData.platformNameLowerCase) || this.rebuiltInformation[platformData.platformNameLowerCase].isEmulator === device.isEmulator)) { + packageFilePath = this.rebuiltInformation[platformData.platformNameLowerCase].packageFilePath; + await this.$deviceInstallAppService.installOnDevice(device, buildData, packageFilePath); + } else { + const shouldBuild = prepareResultData.hasNativeChanges || await this.$buildController.shouldBuild(buildData); + if (shouldBuild) { + packageFilePath = await deviceDescriptor.buildAction(); + this.rebuiltInformation[platformData.platformNameLowerCase] = { isEmulator: device.isEmulator, platform: platformData.platformNameLowerCase, packageFilePath }; + } 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, liveSyncDeviceData: deviceDescriptor }); + + await this.refreshApplication(projectData, liveSyncResultInfo, null, deviceDescriptor); + + this.emitCore(RunOnDeviceEvents.runOnDeviceExecuted, { + projectDir: projectData.projectDir, + deviceIdentifier: device.deviceInfo.identifier, + applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()], + syncedFiles: liveSyncResultInfo.modifiedFilesData.map(m => m.getLocalPath()), + isFullSync: liveSyncResultInfo.isFullSync + }); + + this.$logger.info(`Successfully synced application ${liveSyncResultInfo.deviceAppData.appIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}.`); + + this.emitCore(RunOnDeviceEvents.runOnDeviceStarted, { + projectDir: projectData.projectDir, + deviceIdentifier: device.deviceInfo.identifier, + applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()] + }); + } catch (err) { + this.$logger.warn(`Unable to apply changes on device: ${device.deviceInfo.identifier}. Error is: ${err.message}.`); + + this.emitCore(RunOnDeviceEvents.runOnDeviceError, { + projectDir: projectData.projectDir, + deviceIdentifier: device.deviceInfo.identifier, + applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()], + error: 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: ILiveSyncDeviceDescriptor[]): Promise { + this.rebuiltInformation = {}; + + 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(liveSyncInfo.projectDir, device.deviceInfo.platform, + { + ...liveSyncInfo, + ...deviceDescriptor.buildData, + nativePrepare: { skipNativePrepare: !!deviceDescriptor.skipNativePrepare }, + watch: !liveSyncInfo.skipWatcher, + }); + + try { + if (data.hasNativeChanges) { + const rebuiltInfo = this.rebuiltInformation[platformData.platformNameLowerCase] && (this.$mobileHelper.isAndroidPlatform(platformData.platformNameLowerCase) || this.rebuiltInformation[platformData.platformNameLowerCase].isEmulator === device.isEmulator); + if (!rebuiltInfo) { + await this.$prepareNativePlatformService.prepareNativePlatform(platformData, projectData, prepareData); + await deviceDescriptor.buildAction(); + this.rebuiltInformation[platformData.platformNameLowerCase] = { isEmulator: device.isEmulator, platform: platformData.platformNameLowerCase, packageFilePath: null }; + } + + await this.$deviceInstallAppService.installOnDevice(device, deviceDescriptor.buildData, this.rebuiltInformation[platformData.platformNameLowerCase].packageFilePath); + } + + 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 = { + liveSyncDeviceData: 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, data, deviceDescriptor); + + this.emitCore(RunOnDeviceEvents.runOnDeviceExecuted, { + projectDir: projectData.projectDir, + deviceIdentifier: device.deviceInfo.identifier, + applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()], + syncedFiles: liveSyncResultInfo.modifiedFilesData.map(m => m.getLocalPath()), + isFullSync: liveSyncResultInfo.isFullSync + }); + + 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, data, deviceDescriptor); + + this.emitCore(RunOnDeviceEvents.runOnDeviceExecuted, { + projectDir: projectData.projectDir, + deviceIdentifier: device.deviceInfo.identifier, + applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()], + syncedFiles: liveSyncResultInfo.modifiedFilesData.map(m => m.getLocalPath()), + isFullSync: liveSyncResultInfo.isFullSync + }); + } + } + + 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.emitCore(RunOnDeviceEvents.runOnDeviceError, { + projectDir: projectData.projectDir, + deviceIdentifier: device.deviceInfo.identifier, + applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()], + error: err, + }); + } + } + } + }; + + await this.addActionToChain(projectData.projectDir, () => this.$devicesService.execute(deviceAction, (device: Mobile.IDevice) => { + const liveSyncProcessInfo = this.$liveSyncProcessDataService.getPersistedData(projectData.projectDir); + return (data.platform.toLowerCase() === device.deviceInfo.platform.toLowerCase()) && liveSyncProcessInfo && _.some(liveSyncProcessInfo.deviceDescriptors, deviceDescriptor => deviceDescriptor.identifier === device.deviceInfo.identifier); + })); + } + + private async addActionToChain(projectDir: string, action: () => Promise): Promise { + const liveSyncInfo = this.$liveSyncProcessDataService.getPersistedData(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 emitCore(event: string, data: ILiveSyncEventData): void { + this.$logger.trace(`Will emit event ${event} with data`, data); + this.emit(event, data); + } +} +$injector.register("runController", RunController); diff --git a/lib/data/build-data.ts b/lib/data/build-data.ts new file mode 100644 index 0000000000..08d15e99ff --- /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 || data.forDevice; + 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 || data.aab; + } +} 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..5018a1b9ae --- /dev/null +++ b/lib/data/run-data.ts @@ -0,0 +1,5 @@ +export class RunData { + constructor(public projectDir: string, + public liveSyncInfo: ILiveSyncInfo, + public deviceDescriptors: ILiveSyncDeviceDescriptor[]) { } +} diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index 9a96673a2e..4ea757d53e 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -433,19 +433,10 @@ interface IOpener { open(target: string, appname: string): void; } -interface IBundle { - bundle: boolean; -} - interface IBundleString { bundle: string; } -interface IPlatformTemplate { - platformTemplate: string; -} - - interface IClean { clean: boolean; } @@ -474,10 +465,6 @@ interface INpmInstallConfigurationOptions extends INpmInstallConfigurationOption disableNpmInstall: boolean; } -interface ICreateProjectOptions extends INpmInstallConfigurationOptionsBase { - pathToTemplate?: string; -} - interface IGenerateOptions { collection?: string; } @@ -501,7 +488,7 @@ interface IAndroidBundleOptions { aab: boolean; } -interface IOptions extends IRelease, IDeviceIdentifier, IJustLaunch, IAvd, IAvailableDevices, IProfileDir, IHasEmulatorOption, IBundleString, IPlatformTemplate, IHasEmulatorOption, IClean, IProvision, ITeamIdentifier, IAndroidReleaseOptions, IAndroidBundleOptions, INpmInstallConfigurationOptions, IPort, IEnvOptions, IPluginSeedOptions, IGenerateOptions { +interface IOptions extends IRelease, IDeviceIdentifier, IJustLaunch, IAvd, IAvailableDevices, IProfileDir, IHasEmulatorOption, IBundleString, IHasEmulatorOption, IClean, IProvision, ITeamIdentifier, IAndroidReleaseOptions, IAndroidBundleOptions, INpmInstallConfigurationOptions, IPort, IEnvOptions, IPluginSeedOptions, IGenerateOptions { argv: IYargArgv; validateOptions(commandSpecificDashedOptions?: IDictionary, projectData?: IProjectData): void; options: IDictionary; @@ -530,7 +517,6 @@ interface IOptions extends IRelease, IDeviceIdentifier, IJustLaunch, IAvd, IAvai file: string; analyticsClient: string; force: boolean; - companion: boolean; sdk: string; template: string; certificate: string; @@ -563,7 +549,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; @@ -572,8 +557,6 @@ interface IOptions extends IRelease, IDeviceIdentifier, IJustLaunch, IAvd, IAvai analyticsLogFile: string; performance: Object; cleanupLogFile: string; - workflow: any; - printMessagesForDeprecatedOptions(logger: ILogger): void; } interface IEnvOptions { @@ -586,28 +569,22 @@ 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 { } interface IRunPlatformOptions extends IJustLaunch, IDeviceEmulator { } -interface IDeployPlatformOptions extends IAndroidReleaseOptions, IPlatformTemplate, IRelease, IClean, IDeviceEmulator, IProvision, ITeamIdentifier, IProjectDir { +interface IDeployPlatformOptions extends IAndroidReleaseOptions, IRelease, IClean, IDeviceEmulator, IProvision, ITeamIdentifier, IProjectDir { forceInstall?: boolean; } -interface IUpdatePlatformOptions extends IPlatformTemplate { +interface IUpdatePlatformOptions { currentVersion: string; newVersion: string; canUpdate: boolean; } -interface IProjectInitService { - initialize(): Promise; -} - interface IInfoService { printComponentsInfo(): Promise; } @@ -848,10 +825,10 @@ interface IXcprojService { /** * Returns the path to the xcodeproj file * @param projectData Information about the project. - * @param platformData Information about the platform. + * @param projectRoot The root folder of native project. * @return {string} The full path to the xcodeproj */ - getXcodeprojPath(projectData: IProjectData, platformData: IPlatformData): string; + getXcodeprojPath(projectData: IProjectData, projectRoot: string): string; /** * Checks whether the system needs xcproj to execute ios builds successfully. * In case the system does need xcproj but does not have it, prints an error message. @@ -918,18 +895,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. */ @@ -1038,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..0733fbed91 --- /dev/null +++ b/lib/definitions/build.d.ts @@ -0,0 +1,49 @@ +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 { + getAllAppPackages(buildOutputPath: string, validBuildOutputData: IValidBuildOutputData): IApplicationPackage[]; + getLatestAppPackagePath(platformData: IPlatformData, buildOutputOptions: IBuildOutputOptions): Promise; + copyLatestAppPackage(targetPath: string, platformData: IPlatformData, buildOutputOptions: IBuildOutputOptions): void; +} + +interface IBuildInfoFileService { + getLocalBuildInfo(platformData: IPlatformData, buildData: IBuildData): IBuildInfo; + getDeviceBuildInfo(device: Mobile.IDevice, projectData: IProjectData): Promise; + saveLocalBuildInfo(platformData: IPlatformData, buildInfoFileDirname: string): void; + saveDeviceBuildInfo(device: Mobile.IDevice, projectData: IProjectData, outputFilePath: string): Promise; +} \ 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..b835ab261b 100644 --- a/lib/definitions/debug.d.ts +++ b/lib/definitions/debug.d.ts @@ -1,26 +1,5 @@ -/** - * Describes information for starting debug process. - */ -interface IDebugData extends IAppDebugData, Mobile.IDeviceIdentifier { -} - -/** - * Describes information for application that will be debugged. - */ -interface IAppDebugData extends IProjectDir { - /** - * Application identifier of the app that it will be debugged. - */ +interface IDebugData extends IProjectDir, Mobile.IDeviceIdentifier, IOptionalDebuggingOptions { applicationIdentifier: string; - - /** - * Path to .app built for iOS Simulator. - */ - pathToAppPackage?: string; - - /** - * The name of the application, for example `MyProject`. - */ projectName?: string; } @@ -108,33 +87,12 @@ interface IDebugOptions { interface IDebugDataService { /** * Creates the debug data based on specified options. + * @param {string} deviceIdentifier The identifier of the device * @param {IProjectData} projectData The data describing project that will be debugged. - * @param {IOptions} options The options based on which debugData will be created + * @param {IDebugOptions} debugOptions The debug options * @returns {IDebugData} Data describing the required information for starting debug process. */ - createDebugData(projectData: IProjectData, options: IDeviceIdentifier): IDebugData; -} - -/** - * Describes methods for debug operation. - */ -interface IDebugServiceBase extends NodeJS.EventEmitter { - /** - * Starts debug operation based on the specified debug data. - * @param {IDebugData} debugData Describes information for device and application that will be debugged. - * @param {IDebugOptions} debugOptions Describe possible options to modify the behaivor of the debug operation, for example stop on the first line. - * @returns {Promise} Device Identifier, full url and port where the frontend client can be connected. - */ - debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise; -} - -interface IDebugService extends IDebugServiceBase { - /** - * Stops debug operation for a specific device. - * @param {string} deviceIdentifier Identifier of the device fo which debugging will be stopped. - * @returns {Promise} - */ - debugStop(deviceIdentifier: string): Promise; + getDebugData(deviceIdentifier: string, projectData: IProjectData, debugOptions: IDebugOptions): IDebugData; } /** @@ -159,3 +117,25 @@ interface IDeviceDebugService extends IPlatform, NodeJS.EventEmitter { interface IDebugResultInfo { debugUrl: string; } + +interface IAppDebugData extends IProjectDir { + /** + * Application identifier of the app that it will be debugged. + */ + applicationIdentifier: string; + + /** + * The name of the application, for example `MyProject`. + */ + projectName?: string; +} + +interface IDebugController { + startDebug(debugData: IDebugData): Promise; + stopDebug(deviceIdentifier: string): Promise; + printDebugInformation(debugInformation: IDebugInformation, fireDebuggerAttachedEvent?: boolean): IDebugInformation; + enableDebuggingCoreWithoutWaitingCurrentAction(projectDir: string, deviceIdentifier: string, debugOptions: IDebugOptions): Promise; + enableDebugging(enableDebuggingData: IEnableDebuggingData): Promise[]; + disableDebugging(disableDebuggingData: IDisableDebuggingData): Promise; + attachDebugger(attachDebuggerData: IAttachDebuggerData): Promise; +} diff --git a/lib/definitions/deploy.d.ts b/lib/definitions/deploy.d.ts new file mode 100644 index 0000000000..9dc839421a --- /dev/null +++ b/lib/definitions/deploy.d.ts @@ -0,0 +1,3 @@ +interface IDeployController { + deploy(data: IRunData): Promise; +} \ No newline at end of file diff --git a/lib/definitions/gradle.d.ts b/lib/definitions/gradle.d.ts new file mode 100644 index 0000000000..5e40bb1ed6 --- /dev/null +++ b/lib/definitions/gradle.d.ts @@ -0,0 +1,20 @@ +interface IGradleCommandService { + executeCommand(gradleArgs: string[], options: IGradleCommandOptions): Promise; +} + +interface IGradleCommandOptions { + cwd: string; + message?: string; + stdio?: string; + spawnOptions?: ISpawnFromEventOptions; +} + +interface IGradleBuildService { + buildProject(projectRoot: string, buildData: IAndroidBuildData): Promise; + cleanProject(projectRoot: string, buildData: IAndroidBuildData): Promise; +} + +interface IGradleBuildArgsService { + getBuildTaskArgs(buildData: IAndroidBuildData): string[]; + getCleanTaskArgs(buildData: IAndroidBuildData): string[]; +} 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..b224149091 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 ILiveSyncProcessData { timer: NodeJS.Timer; - watcherInfo: { - watcher: IFSWatcher, - patterns: string[] - }; actionsChain: Promise; isStopped: boolean; - deviceDescriptors: ILiveSyncDeviceInfo[]; + deviceDescriptors: ILiveSyncDeviceDescriptor[]; currentSyncAction: Promise; syncToPreviewApp: boolean; + platforms: string[]; } interface IOptionalOutputPath { @@ -109,7 +48,7 @@ declare global { /** * Describes information for LiveSync on a device. */ - interface ILiveSyncDeviceInfo extends IOptionalOutputPath, IOptionalDebuggingOptions { + interface ILiveSyncDeviceDescriptor extends IOptionalDebuggingOptions { /** * Device identifier. */ @@ -128,30 +67,24 @@ declare global { /** * Whether debugging has been enabled for this device or not */ - debugggingEnabled?: boolean; + debuggingEnabled?: boolean; /** - * Describes options specific for each platform, like provision for iOS, target sdk for Android, etc. + * Describes the data used for building the application */ - platformSpecificOptions?: IPlatformOptions; - } - - interface IOptionalSkipWatcher { - /** - * Defines if the watcher should be skipped. If not passed, fs.Watcher will be started. - */ - skipWatcher?: boolean; + buildData: IBuildData; } /** * Describes a LiveSync operation. */ - interface ILiveSyncInfo extends IProjectDir, IEnvOptions, IBundle, IRelease, IOptionalSkipWatcher, IHasUseHotModuleReloadOption, IHasSyncToPreviewAppOption { + interface ILiveSyncInfo extends IProjectDir, IEnvOptions, IRelease, IHasUseHotModuleReloadOption { + 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,13 +103,8 @@ declare global { * If not provided, defaults to 10seconds. */ timeout?: string; - } - interface IHasSyncToPreviewAppOption { - /** - * Defines if the livesync should be executed in preview app on device. - */ - syncToPreviewApp?: boolean; + nativePrepare?: INativePrepare; } interface IHasUseHotModuleReloadOption { @@ -196,54 +124,25 @@ 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. */ interface ILiveSyncService extends EventEmitter { /** * Starts LiveSync operation by rebuilding the application if necessary and starting watcher. - * @param {ILiveSyncDeviceInfo[]} deviceDescriptors Describes each device for which we would like to sync the application - identifier, outputPath and action to rebuild the app. + * @param {ILiveSyncDeviceDescriptor[]} deviceDescriptors Describes each device for which we would like to sync the application - identifier, outputPath and action to rebuild the app. * @param {ILiveSyncInfo} liveSyncData Describes the LiveSync operation - for which project directory is the operation and other settings. * @returns {Promise} */ - liveSync(deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise; + liveSync(deviceDescriptors: ILiveSyncDeviceDescriptor[], liveSyncData: ILiveSyncInfo): Promise; /** * Starts LiveSync operation to Preview app. @@ -266,60 +165,11 @@ declare global { * In case LiveSync has been started on many devices, but stopped for some of them at a later point, * calling the method after that will return information only for devices for which LiveSync operation is in progress. * @param {string} projectDir The path to project for which the LiveSync operation is executed - * @returns {ILiveSyncDeviceInfo[]} Array of elements describing parameters used to start LiveSync on each device. - */ - 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. + * @returns {ILiveSyncDeviceDescriptor[]} Array of elements describing parameters used to start LiveSync on each device. */ - attachDebugger(settings: IAttachDebuggerOptions): Promise; + getLiveSyncDeviceDescriptors(projectDir: string): ILiveSyncDeviceDescriptor[]; } - /** - * Describes additional debugging settings. - */ - interface IDebuggingAdditionalOptions extends IProjectDir { } - /** * Describes settings used when disabling debugging. */ @@ -332,10 +182,15 @@ declare global { debugOptions?: IDebugOptions; } - /** - * Describes settings used when enabling debugging. - */ - interface IEnableDebuggingDeviceOptions extends Mobile.IDeviceIdentifier, IOptionalDebuggingOptions { } + interface IEnableDebuggingData extends IProjectDir, IOptionalDebuggingOptions { + deviceIdentifiers: string[]; + } + + interface IDisableDebuggingData extends IProjectDir { + deviceIdentifiers: string[]; + } + + interface IAttachDebuggerData extends IProjectDir, Mobile.IDeviceIdentifier, IOptionalDebuggingOptions, IIsEmulator, IPlatform, IOptionalOutputPath { } /** * Describes settings passed to livesync service in order to control event emitting during refresh application. @@ -345,12 +200,6 @@ declare global { shouldCheckDeveloperDiscImage: boolean; } - /** - * Describes settings used for attaching a debugger. - */ - interface IAttachDebuggerOptions extends IDebuggingAdditionalOptions, IEnableDebuggingDeviceOptions, IIsEmulator, IPlatform, IOptionalOutputPath { - } - interface IConnectTimeoutOption { /** * Time to wait for successful connection. Defaults to 30000 miliseconds. @@ -362,8 +211,7 @@ declare global { filesToRemove: string[]; filesToSync: string[]; isReinstalled: boolean; - syncAllFiles: boolean; - liveSyncDeviceInfo: ILiveSyncDeviceInfo; + liveSyncDeviceData: ILiveSyncDeviceDescriptor; hmrData: IPlatformHmrData; force?: boolean; } @@ -381,8 +229,7 @@ declare global { interface IFullSyncInfo extends IProjectDataComposition, IHasUseHotModuleReloadOption, IConnectTimeoutOption { device: Mobile.IDevice; watch: boolean; - syncAllFiles: boolean; - liveSyncDeviceInfo: ILiveSyncDeviceInfo; + liveSyncDeviceData: ILiveSyncDeviceDescriptor; force?: boolean; } @@ -404,6 +251,7 @@ declare global { shouldRestart(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise; getDeviceLiveSyncService(device: Mobile.IDevice, projectData: IProjectData): INativeScriptDeviceLiveSyncService; } + interface IRestartApplicationInfo { didRestart: boolean; } @@ -449,7 +297,7 @@ declare global { * @param {boolean} isFullSync Indicates if the operation is part of a fullSync * @return {Promise} Returns the ILocalToDevicePathData of all transfered files */ - transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo, options: ITransferFilesOptions): Promise; + transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceData: ILiveSyncDeviceDescriptor, options: ITransferFilesOptions): Promise; } interface IAndroidNativeScriptDeviceLiveSyncService extends INativeScriptDeviceLiveSyncService { @@ -578,7 +426,6 @@ declare global { interface IDeviceProjectRootOptions { appIdentifier: string; getDirname?: boolean; - syncAllFiles?: boolean; watch?: boolean; } @@ -590,7 +437,7 @@ declare global { /** * Describes additional options, that can be passed to LiveSyncCommandHelper. */ - interface ILiveSyncCommandHelperAdditionalOptions extends IBuildPlatformAction, INativePrepare, IHasSyncToPreviewAppOption { + interface ILiveSyncCommandHelperAdditionalOptions extends IBuildPlatformAction, INativePrepare { /** * A map representing devices which have debugging enabled initially. */ @@ -629,6 +476,20 @@ declare global { * @returns {Promise} */ executeCommandLiveSync(platform?: string, additionalOptions?: ILiveSyncCommandHelperAdditionalOptions): Promise; + createDeviceDescriptors(devices: Mobile.IDevice[], platform: string, additionalOptions?: ILiveSyncCommandHelperAdditionalOptions): Promise; + getDeviceInstances(platform?: string): Promise; + getLiveSyncData(projectDir: string): ILiveSyncInfo; } -} \ No newline at end of file + interface ILiveSyncServiceResolver { + resolveLiveSyncService(platform: string): IPlatformLiveSyncService; + } + + interface ILiveSyncProcessDataService { + getPersistedData(projectDir: string): ILiveSyncProcessData; + getDeviceDescriptors(projectDir: string): ILiveSyncDeviceDescriptor[]; + getAllPersistedData(): IDictionary; + persistData(projectDir: string, deviceDescriptors: ILiveSyncDeviceDescriptor[], platforms: string[]): void; + hasDeviceDescriptors(projectDir: string): boolean; + } +} diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts index 2ded89ea0f..46406ade21 100644 --- a/lib/definitions/platform.d.ts +++ b/lib/definitions/platform.d.ts @@ -14,250 +14,14 @@ interface IBuildPlatformAction { buildPlatform(platform: string, buildConfig: IBuildConfig, projectData: IProjectData): Promise; } -interface IWorkflowService { - handleLegacyWorkflow(options: IHandleLegacyWorkflowOptions): Promise; -} - -interface IHandleLegacyWorkflowOptions { - projectDir: string; - settings: IWebpackWorkflowSettings; - skipWarnings?: boolean; - force?: boolean; -} - -interface IWebpackWorkflowSettings { - bundle?: boolean | string; - useHotModuleReload?: boolean; - release?: boolean; -} - -interface IPlatformService extends IBuildPlatformAction, NodeJS.EventEmitter { - cleanPlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, config: IPlatformOptions, framework?: string): Promise; - - addPlatforms(platforms: string[], platformTemplate: 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[], platformTemplate: 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[]; @@ -274,33 +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 IAppFilesUpdaterOptionsComposition { - appFilesUpdaterOptions: IAppFilesUpdaterOptions; +interface IBuildOutputOptions extends Partial, IRelease, IHasAndroidBundle { + outputPath?: string; } -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; - prepareJSNodeModules(opts: INodeModulesBuilderData): Promise; + prepareNodeModules(platformData: IPlatformData, projectData: IProjectData): Promise; } interface INodeModulesDependenciesBuilder { @@ -319,72 +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; - platformTemplate?: string; -} - -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, IPlatformTemplate, 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; } @@ -401,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 2f0d775666..dd59b45d32 100644 --- a/lib/definitions/plugins.d.ts +++ b/lib/definitions/plugins.d.ts @@ -1,10 +1,8 @@ interface IPluginsService { add(plugin: string, projectData: IProjectData): Promise; // adds plugin by name, github url, local path and et. remove(pluginName: string, projectData: IProjectData): Promise; // removes plugin only by name - prepare(pluginData: IDependencyData, platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise; getAllInstalledPlugins(projectData: IProjectData): Promise; ensureAllDependenciesAreInstalled(projectData: IProjectData): Promise; - preparePluginScripts(pluginData: IPluginData, platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): void /** * Returns all dependencies and devDependencies from pacakge.json file. @@ -12,7 +10,6 @@ interface IPluginsService { * @returns {IPackageJsonDepedenciesResult} */ getDependenciesFromPackageJson(projectDir: string): IPackageJsonDepedenciesResult; - validate(platformData: IPlatformData, projectData: IProjectData): Promise; preparePluginNativeCode(pluginData: IPluginData, platform: string, projectData: IProjectData): Promise; convertToPluginData(cacheData: any, projectDir: string): IPluginData; isNativeScriptPlugin(pluginPackageJsonPath: string): boolean; @@ -29,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; @@ -46,53 +43,6 @@ interface IPluginPlatformsData { android: string; } -interface IPluginVariablesService { - /** - * Saves plugin variables in project package.json file. - * @param {IPluginData} pluginData for the plugin. - * @param {string} projectDir: Specifies the directory of the project. - * @return {Promise} - */ - savePluginVariablesInProjectFile(pluginData: IPluginData, projectDir: string): Promise; - - /** - * Removes plugin variables from project package.json file. - * @param {string} pluginName Name of the plugin. - * @param {string} projectDir: Specifies the directory of the project. - * @return {void} - */ - removePluginVariablesFromProjectFile(pluginName: string, projectDir: string): void; - - /** - * Replaces all plugin variables with their corresponding values. - * @param {IPluginData} pluginData for the plugin. - * @param {pluginConfigurationFilePath} pluginConfigurationFilePath for the plugin. - * @param {string} projectDir: Specifies the directory of the project. - * @return {Promise} - */ - interpolatePluginVariables(pluginData: IPluginData, pluginConfigurationFilePath: string, projectDir: string): Promise; - - /** - * Replaces {nativescript.id} expression with the application identifier from package.json. - * @param {pluginConfigurationFilePath} pluginConfigurationFilePath for the plugin. - * @return {void} - */ - interpolateAppIdentifier(pluginConfigurationFilePath: string, projectIdentifier: string): void; - - /** - * Replaces both plugin variables and appIdentifier - */ - interpolate(pluginData: IPluginData, pluginConfigurationFilePath: string, projectDir: string, projectIdentifier: string): Promise; - - /** - * Returns the - * @param {string} pluginName for the plugin. - * @return {Promise} returns the changed plugin configuration file content. - */ - getPluginVariablePropertyName(pluginName: string): string; - -} - interface IPluginVariableData { defaultValue?: string; name?: string; diff --git a/lib/definitions/prepare.d.ts b/lib/definitions/prepare.d.ts new file mode 100644 index 0000000000..c8ca2e0012 --- /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 IiOSCodeSigningData { + teamId: string; + provision: string; + mobileProvisionData: any; + } + + interface IiOSPrepareData extends IPrepareData, IiOSCodeSigningData { } + + 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; + } +} diff --git a/lib/definitions/preview-app-livesync.d.ts b/lib/definitions/preview-app-livesync.d.ts index 123ae2e721..414b4cffe7 100644 --- a/lib/definitions/preview-app-livesync.d.ts +++ b/lib/definitions/preview-app-livesync.d.ts @@ -2,12 +2,6 @@ import { FilePayload, Device, FilesPayload } from "nativescript-preview-sdk"; import { EventEmitter } from "events"; declare global { - interface IPreviewAppLiveSyncService extends EventEmitter { - initialize(data: IPreviewAppLiveSyncData): void; - syncFiles(data: IPreviewAppLiveSyncData, filesToSync: string[], filesToRemove: string[]): Promise; - stopLiveSync(): Promise; - } - interface IPreviewAppFilesService { getInitialFilesPayload(liveSyncData: IPreviewAppLiveSyncData, platform: string, deviceId?: string): FilesPayload; getFilesPayload(liveSyncData: IPreviewAppLiveSyncData, filesData: IPreviewAppFilesData, platform: string, deviceId?: string): FilesPayload; @@ -18,7 +12,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; @@ -80,4 +74,9 @@ declare global { subscribeKey: string; default?: boolean; } + + interface IPreviewAppController { + startPreview(data: IPreviewAppLiveSyncData): Promise; + stopPreview(): Promise; + } } \ No newline at end of file diff --git a/lib/definitions/project-changes.d.ts b/lib/definitions/project-changes.d.ts index 7da52ca180..e799efbb5d 100644 --- a/lib/definitions/project-changes.d.ts +++ b/lib/definitions/project-changes.d.ts @@ -13,13 +13,9 @@ 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 +23,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 43de3c8005..0d72cc2f79 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -142,14 +142,6 @@ interface IProjectDataService { */ setNSValue(projectDir: string, key: string, value: any): void; - /** - * Sets a value in the `useLegacyWorkflow` key in a project's nsconfig.json. - * @param {string} projectDir The project directory - the place where the root package.json is located. - * @param {any} value Value of the key to be set to `useLegacyWorkflow` key in project's nsconfig.json. - * @returns {void} - */ - setUseLegacyWorkflow(projectDir: string, value: any): void; - /** * Removes a property from `nativescript` key in project's package.json. * @param {string} projectDir The project directory - the place where the root package.json is located. @@ -349,10 +341,9 @@ interface ILocalBuildService { * Builds a project locally. * @param {string} platform Platform for which to build. * @param {IPlatformBuildData} platformBuildOptions Additional options for controlling the build. - * @param {string} platformTemplate The name of the template. * @return {Promise} Path to the build output. */ - build(platform: string, platformBuildOptions: IPlatformBuildData, platformTemplate?: string): Promise; + build(platform: string, platformBuildOptions: IPlatformBuildData): Promise; /** * Removes build artifacts specific to the platform * @param {ICleanNativeAppData} data Data describing the clean app process @@ -363,140 +354,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; - - validatePlugins(projectData: IProjectData): 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 {string} projectRoot The root directory of the native project. - * @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; - - /** - * Build native part of a nativescript plugins if necessary - */ - prebuildNativePlugin(buildOptions: IPluginBuildOptions): Promise; - - /** - * Traverse through the production dependencies and find plugins that need build/rebuild - */ - checkIfPluginsNeedBuild(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: ILiveSyncDeviceDescriptor[]): Promise; canStartKarmaServer(projectData: IProjectData): Promise; } @@ -604,6 +467,7 @@ interface IIOSExtensionsService { interface IIOSWatchAppService { addWatchAppFromPath(options: IAddWatchAppFromPathOptions): Promise; removeWatchApp(options: IRemoveWatchAppOptions): void; + hasWatchApp(platformData: IPlatformData, projectData: IProjectData): boolean; } interface IAddTargetFromPathOptions { diff --git a/lib/definitions/run.d.ts b/lib/definitions/run.d.ts new file mode 100644 index 0000000000..3dfe93169a --- /dev/null +++ b/lib/definitions/run.d.ts @@ -0,0 +1,32 @@ +import { EventEmitter } from "events"; + +declare global { + + interface IRunData { + liveSyncInfo: ILiveSyncInfo; + deviceDescriptors: ILiveSyncDeviceDescriptor[]; + } + + interface IDeployData { + buildData: IBuildData; + deviceDescriptors: ILiveSyncDeviceDescriptor[]; + } + + interface IStopRunData { + projectDir: string; + deviceIdentifiers?: string[]; + stopOptions?: { shouldAwaitAllActions: boolean }; + } + + interface IRunController extends EventEmitter { + run(runData: IRunData): Promise; + stop(data: IStopRunData): Promise; + getDeviceDescriptors(data: { projectDir: string }): ILiveSyncDeviceDescriptor[]; + } + + interface IDeviceInstallAppService { + installOnDevice(device: Mobile.IDevice, buildData: IBuildData, packageFile?: string): Promise; + installOnDeviceIfNeeded(device: Mobile.IDevice, buildData: IBuildData, packageFile?: string): Promise; + shouldInstall(device: Mobile.IDevice, buildData: IBuildData): Promise; + } +} diff --git a/lib/definitions/xcode.d.ts b/lib/definitions/xcode.d.ts index 01c561dab3..861f14b309 100644 --- a/lib/definitions/xcode.d.ts +++ b/lib/definitions/xcode.d.ts @@ -1,62 +1,62 @@ declare module "nativescript-dev-xcode" { - interface Options { - [key: string]: any; - - customFramework?: boolean; - embed?: boolean; - relativePath?: string; - } - - class project { - constructor(filename: string); - - parse(callback: () => void): void; - parseSync(): void; - - writeSync(options: any): string; - - addFramework(filepath: string, options?: Options): void; - removeFramework(filePath: string, options?: Options): void; - - addPbxGroup(filePathsArray: any[], name: string, path: string, sourceTree: string): void; - - removePbxGroup(groupName: string, path: string): void; - - addToHeaderSearchPaths(options?: Options): void; - removeFromHeaderSearchPaths(options?: Options): void; - updateBuildProperty(key: string, value: any): void; - - pbxXCBuildConfigurationSection(): any; - - addTarget(targetName: string, targetType: string, targetPath?: string, parentTarget?: string): target; - addBuildPhase(filePathsArray: string[], - buildPhaseType: string, - comment: string, - target?: string, - optionsOrFolderType?: Object|string, - subfolderPath?: string - ): any; - addToBuildSettings(buildSetting: string, value: any, targetUuid?: string): void; - addPbxGroup( - filePathsArray: string[], - name: string, - path: string, - sourceTree: string, - opt: {filesRelativeToProject?: boolean, target?: string, uuid?: string, isMain?: boolean } - ): group; - addBuildProperty(prop: string, value: any, build_name?: string, productName?: string): void; - addToHeaderSearchPaths(file: string|Object, productName?: string): void; - removeTargetsByProductType(targetType: string): void - getFirstTarget(): {uuid: string} - } - - class target { - uuid: string; - pbxNativeTarget: {productName: string} - } - - class group { - uuid: string; - pbxGroup: Object; - } + interface Options { + [key: string]: any; + + customFramework?: boolean; + embed?: boolean; + relativePath?: string; + } + + class project { + constructor(filename: string); + + parse(callback: () => void): void; + parseSync(): void; + + writeSync(options: any): string; + + addFramework(filepath: string, options?: Options): void; + removeFramework(filePath: string, options?: Options): void; + + addPbxGroup(filePathsArray: any[], name: string, path: string, sourceTree: string): void; + + removePbxGroup(groupName: string, path: string): void; + + addToHeaderSearchPaths(options?: Options): void; + removeFromHeaderSearchPaths(options?: Options): void; + updateBuildProperty(key: string, value: any): void; + + pbxXCBuildConfigurationSection(): any; + + addTarget(targetName: string, targetType: string, targetPath?: string, parentTarget?: string): target; + addBuildPhase(filePathsArray: string[], + buildPhaseType: string, + comment: string, + target?: string, + optionsOrFolderType?: Object|string, + subfolderPath?: string + ): any; + addToBuildSettings(buildSetting: string, value: any, targetUuid?: string): void; + addPbxGroup( + filePathsArray: string[], + name: string, + path: string, + sourceTree: string, + opt: {filesRelativeToProject?: boolean, target?: string, uuid?: string, isMain?: boolean } + ): group; + addBuildProperty(prop: string, value: any, build_name?: string, productName?: string): void; + addToHeaderSearchPaths(file: string|Object, productName?: string): void; + removeTargetsByProductType(targetType: string): void; + getFirstTarget(): {uuid: string}; + } + + class target { + uuid: string; + pbxNativeTarget: {productName: string} + } + + class group { + uuid: string; + pbxGroup: Object; + } } \ 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/helpers/bundle-validator-helper.ts b/lib/helpers/bundle-validator-helper.ts index d5d8e6de0d..657208e038 100644 --- a/lib/helpers/bundle-validator-helper.ts +++ b/lib/helpers/bundle-validator-helper.ts @@ -13,16 +13,14 @@ export class BundleValidatorHelper extends VersionValidatorHelper implements IBu } public validate(projectData: IProjectData, minSupportedVersion?: string): void { - if (this.$options.bundle) { - const currentVersion = this.getBundlerDependencyVersion(projectData); - if (!currentVersion) { - this.$errors.failWithoutHelp(BundleValidatorMessages.MissingBundlePlugin); - } - - const shouldThrowError = minSupportedVersion && this.isValidVersion(currentVersion) && this.isVersionLowerThan(currentVersion, minSupportedVersion); - if (shouldThrowError) { - this.$errors.failWithoutHelp(util.format(BundleValidatorMessages.NotSupportedVersion, minSupportedVersion)); - } + const currentVersion = this.getBundlerDependencyVersion(projectData); + if (!currentVersion) { + this.$errors.failWithoutHelp(BundleValidatorMessages.MissingBundlePlugin); + } + + 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 de2d02f31a..c010c96522 100644 --- a/lib/helpers/deploy-command-helper.ts +++ b/lib/helpers/deploy-command-helper.ts @@ -1,46 +1,59 @@ -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 $buildDataService: IBuildDataService, + 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, - projectDir: this.$projectData.projectDir, - emulator: this.$options.emulator, - platformTemplate: this.$options.platformTemplate, - 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 = { + public async deploy(platform: string, additionalOptions?: ILiveSyncCommandHelperAdditionalOptions) { + const emulator = this.$options.emulator; + await this.$devicesService.initialize({ + deviceId: this.$options.device, platform, - appFilesUpdaterOptions, - deployOptions, - projectData: this.$projectData, - buildPlatform: this.$platformService.buildPlatform.bind(this.$platformService), - config: this.$options, - env: this.$options.env, + emulator, + skipInferPlatform: !platform, + sdk: this.$options.sdk + }); + + const devices = this.$devicesService.getDeviceInstances() + .filter(d => !platform || d.deviceInfo.platform.toLowerCase() === platform.toLowerCase()); + + const deviceDescriptors: ILiveSyncDeviceDescriptor[] = devices + .map(d => { + const outputPath = additionalOptions && additionalOptions.getOutputDirectory && additionalOptions.getOutputDirectory({ + platform: d.deviceInfo.platform, + emulator: d.isEmulator, + projectDir: this.$projectData.projectDir + }); + + const buildData = this.$buildDataService.getBuildData(this.$projectData.projectDir, d.deviceInfo.platform, { ...this.$options, outputPath, buildForDevice: !d.isEmulator }); - }; + const buildAction = additionalOptions && additionalOptions.buildPlatform ? + additionalOptions.buildPlatform.bind(additionalOptions.buildPlatform, d.deviceInfo.platform, buildData, this.$projectData) : + this.$buildController.prepareAndBuild.bind(this.$buildController, d.deviceInfo.platform, buildData, this.$projectData); - return deployPlatformInfo; + const info: ILiveSyncDeviceDescriptor = { + identifier: d.deviceInfo.identifier, + buildAction, + debuggingEnabled: additionalOptions && additionalOptions.deviceDebugMap && additionalOptions.deviceDebugMap[d.deviceInfo.identifier], + debugOptions: this.$options, + skipNativePrepare: additionalOptions && additionalOptions.skipNativePrepare, + buildData + }; + + return info; + }); + + await this.$deployController.deploy({ + buildData: this.$buildDataService.getBuildData(this.$projectData.projectDir, platform, { ...this.$options, skipWatcher: !this.$options.watch }), + deviceDescriptors + }); } } - $injector.register("deployCommandHelper", DeployCommandHelper); diff --git a/lib/helpers/livesync-command-helper.ts b/lib/helpers/livesync-command-helper.ts index 2760220cdb..8c4157a67f 100644 --- a/lib/helpers/livesync-command-helper.ts +++ b/lib/helpers/livesync-command-helper.ts @@ -1,43 +1,53 @@ -import { LiveSyncEvents } from "../constants"; +import { RunOnDeviceEvents } from "../constants"; +import { DeployController } from "../controllers/deploy-controller"; export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { public static MIN_SUPPORTED_WEBPACK_VERSION_WITH_HMR = "0.17.0"; - constructor(private $platformService: IPlatformService, + constructor( + private $buildDataService: IBuildDataService, private $projectData: IProjectData, private $options: IOptions, - private $liveSyncService: ILiveSyncService, + private $deployController: DeployController, private $iosDeviceOperations: IIOSDeviceOperations, private $mobileHelper: Mobile.IMobileHelper, private $devicesService: Mobile.IDevicesService, - private $platformsData: IPlatformsData, + private $injector: IInjector, + private $buildController: IBuildController, private $analyticsService: IAnalyticsService, private $bundleValidatorHelper: IBundleValidatorHelper, private $errors: IErrors, private $iOSSimulatorLogProvider: Mobile.IiOSSimulatorLogProvider, - private $logger: ILogger, - private $cleanupService: ICleanupService) { - } + private $cleanupService: ICleanupService, + private $runController: IRunController + ) { } - 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; - } + // TODO: Remove this and replace it with buildData + public getLiveSyncData(projectDir: string): ILiveSyncInfo { + const liveSyncInfo: ILiveSyncInfo = { + 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 + }; - if (!this.$options.syncAllFiles) { - this.$logger.info("Skipping node_modules folder! Use the syncAllFiles option to sync files from this folder."); - } + return liveSyncInfo; + } - 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,99 +55,71 @@ 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 { - 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."); - } else { - this.$errors.failWithoutHelp("Unable to find applicable devices to execute operation and unable to start emulator when platform is not specified."); - } - } - - const workingWithiOSDevices = !platform || this.$mobileHelper.isiOSPlatform(platform); - const shouldKeepProcessAlive = this.$options.watch || !this.$options.justlaunch; - if (shouldKeepProcessAlive) { - this.$analyticsService.setShouldDispose(false); - this.$cleanupService.setShouldDispose(false); - - if (workingWithiOSDevices) { - this.$iosDeviceOperations.setShouldDispose(false); - this.$iOSSimulatorLogProvider.setShouldDispose(false); - } - } - - if (this.$options.release) { - await this.runInReleaseMode(platform, additionalOptions); - return; - } - + public async createDeviceDescriptors(devices: Mobile.IDevice[], platform: string, additionalOptions?: ILiveSyncCommandHelperAdditionalOptions): Promise { // Now let's take data for each device: - const deviceDescriptors: ILiveSyncDeviceInfo[] = devices + const deviceDescriptors: ILiveSyncDeviceDescriptor[] = devices .map(d => { - let buildAction: IBuildAction; - - 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 outputPath = additionalOptions && additionalOptions.getOutputDirectory && additionalOptions.getOutputDirectory({ + platform: d.deviceInfo.platform, + emulator: d.isEmulator, + projectDir: this.$projectData.projectDir + }); + + const buildData = this.$buildDataService.getBuildData(this.$projectData.projectDir, d.deviceInfo.platform, { ...this.$options.argv, outputPath, buildForDevice: !d.isEmulator }); - 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); + const buildAction = additionalOptions && additionalOptions.buildPlatform ? + additionalOptions.buildPlatform.bind(additionalOptions.buildPlatform, d.deviceInfo.platform, buildData, this.$projectData) : + this.$buildController.build.bind(this.$buildController, buildData); - const info: ILiveSyncDeviceInfo = { + const info: ILiveSyncDeviceDescriptor = { 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 - }), skipNativePrepare: additionalOptions && additionalOptions.skipNativePrepare, + buildData }; return info; }); - const liveSyncInfo: ILiveSyncInfo = { - projectDir: this.$projectData.projectDir, - skipWatcher: !this.$options.watch, - watchAllFiles: this.$options.syncAllFiles, - 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 - }; + 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 { liveSyncInfo, deviceDescriptors } = await this.executeLiveSyncOperationCore(devices, platform, additionalOptions); + + if (this.$options.release) { + await this.runInRelease(platform, deviceDescriptors, liveSyncInfo); + return; + } + + await this.$runController.run({ + liveSyncInfo, + deviceDescriptors + }); const remainingDevicesToSync = devices.map(d => d.deviceInfo.identifier); - this.$liveSyncService.on(LiveSyncEvents.liveSyncStopped, (data: { projectDir: string, deviceIdentifier: string }) => { + this.$runController.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 +127,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; @@ -157,44 +139,52 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { return result; } - private async runInReleaseMode(platform: string, additionalOptions?: ILiveSyncCommandHelperAdditionalOptions): Promise { - const runPlatformOptions: IRunPlatformOptions = { - device: this.$options.device, + private async executeLiveSyncOperationCore(devices: Mobile.IDevice[], platform: string, additionalOptions?: ILiveSyncCommandHelperAdditionalOptions): Promise<{liveSyncInfo: ILiveSyncInfo, deviceDescriptors: ILiveSyncDeviceDescriptor[]}> { + 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."); + } else { + this.$errors.failWithoutHelp("Unable to find applicable devices to execute operation and unable to start emulator when platform is not specified."); + } + } + + const workingWithiOSDevices = !platform || this.$mobileHelper.isiOSPlatform(platform); + const shouldKeepProcessAlive = this.$options.watch || !this.$options.justlaunch; + if (shouldKeepProcessAlive) { + this.$analyticsService.setShouldDispose(false); + this.$cleanupService.setShouldDispose(false); + + if (workingWithiOSDevices) { + this.$iosDeviceOperations.setShouldDispose(false); + this.$iOSSimulatorLogProvider.setShouldDispose(false); + } + } + + const deviceDescriptors = await this.createDeviceDescriptors(devices, platform, additionalOptions); + const liveSyncInfo = this.getLiveSyncData(this.$projectData.projectDir); + + return { liveSyncInfo, deviceDescriptors }; + } + + private async runInRelease(platform: string, deviceDescriptors: ILiveSyncDeviceDescriptor[], liveSyncInfo: ILiveSyncInfo): Promise { + await this.$devicesService.initialize({ + platform, + deviceId: this.$options.device, emulator: this.$options.emulator, - justlaunch: this.$options.justlaunch - }; + skipInferPlatform: !platform, + sdk: this.$options.sdk + }); - const deployOptions = _.merge(({ - projectDir: this.$projectData.projectDir, - clean: true - }), this.$options.argv); + const buildData = this.$buildDataService.getBuildData(liveSyncInfo.projectDir, platform, { ...this.$options.argv, clean: true, watch: false }); - 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 - } - ); + await this.$deployController.deploy({ + buildData, + deviceDescriptors + }); + + 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 }); } } } diff --git a/lib/helpers/platform-command-helper.ts b/lib/helpers/platform-command-helper.ts new file mode 100644 index 0000000000..18e48a280b --- /dev/null +++ b/lib/helpers/platform-command-helper.ts @@ -0,0 +1,192 @@ +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) { + const version: string = this.getCurrentPlatformVersion(platform, projectData); + + await this.removePlatforms([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.info(`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); + const platforms = this.$mobileHelper.platformNames.map(p => p.toLowerCase()); + return _.filter(subDirs, p => platforms.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.info("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 8083b69425..f370b093ef 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -31,27 +31,13 @@ export class Options { this.$errors.failWithoutHelp("The options --release and --hmr cannot be used simultaneously."); } - // HACK: temporary solution for 5.3.0 release (until the webpack only feature) - const parsed = require("yargs-parser")(process.argv.slice(2), { 'boolean-negation': false }); - const noBundle = parsed && (parsed.bundle === false || parsed.bundle === 'false'); - if (noBundle && this.argv.hmr) { - this.$errors.failWithoutHelp("The options --no-bundle and --hmr cannot be used simultaneously."); - } - - if (projectData && projectData.useLegacyWorkflow === false) { - this.argv.bundle = this.argv.bundle !== undefined ? this.argv.bundle : "webpack"; - this.argv.hmr = !this.argv.release; - } + this.argv.bundle = "webpack"; + const parsed = require("yargs-parser")(process.argv.slice(2), { 'boolean-negation': false }); // --no-hmr -> hmr: false or --hmr false -> hmr: 'false' const noHmr = parsed && (parsed.hmr === false || parsed.hmr === 'false'); - if (noHmr) { - this.argv.hmr = false; - } - - if (noBundle) { - this.argv.bundle = undefined; - this.argv.hmr = false; + if (!noHmr) { + this.argv.hmr = !this.argv.release; } if (this.argv.debugBrk) { @@ -102,7 +88,6 @@ export class Options { compileSdk: { type: OptionType.Number, hasSensitiveValue: false }, port: { type: OptionType.Number, hasSensitiveValue: false }, copyTo: { type: OptionType.String, hasSensitiveValue: true }, - platformTemplate: { type: OptionType.String, hasSensitiveValue: true }, js: { type: OptionType.Boolean, hasSensitiveValue: false }, javascript: { type: OptionType.Boolean, hasSensitiveValue: false }, ng: { type: OptionType.Boolean, hasSensitiveValue: false }, @@ -117,7 +102,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 }, @@ -148,9 +132,6 @@ export class Options { justlaunch: { type: OptionType.Boolean, hasSensitiveValue: false }, file: { type: OptionType.String, hasSensitiveValue: true }, force: { type: OptionType.Boolean, alias: "f", hasSensitiveValue: false }, - // remove legacy - workflow: { type: OptionType.Boolean, hasSensitiveValue: false }, - companion: { type: OptionType.Boolean, hasSensitiveValue: false }, emulator: { type: OptionType.Boolean, hasSensitiveValue: false }, sdk: { type: OptionType.String, hasSensitiveValue: false }, template: { type: OptionType.String, hasSensitiveValue: true }, @@ -213,16 +194,6 @@ export class Options { }); } - public printMessagesForDeprecatedOptions($logger: ILogger) { - if (this.argv.platformTemplate) { - $logger.warn(`"--platformTemplate" option has been deprecated and will be removed in the upcoming NativeScript CLI v6.0.0. More info can be found in this issue https://github.com/NativeScript/nativescript-cli/issues/4518.`); - } - - if (this.argv.syncAllFiles) { - $logger.warn(`"--syncAllFiles" option has been deprecated and will be removed in the upcoming NativeScript CLI v6.0.0. More info can be found in this issue https://github.com/NativeScript/nativescript-cli/issues/4518.`); - } - } - private getCorrectOptionName(optionName: string): string { const secondaryOptionName = this.getNonDashedOptionName(optionName); return _.includes(this.optionNames, secondaryOptionName) ? secondaryOptionName : optionName; @@ -309,10 +280,7 @@ export class Options { this.argv.js = true; } - // Default to "nativescript-dev-webpack" if only `--bundle` is passed - if (this.argv.bundle !== undefined || this.argv.hmr) { - this.argv.bundle = this.argv.bundle || "webpack"; - } + this.argv.bundle = "webpack"; this.adjustDashedOptions(); } 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..d6eebdef7d --- /dev/null +++ b/lib/resolvers/livesync-service-resolver.ts @@ -0,0 +1,18 @@ +export class LiveSyncServiceResolver implements ILiveSyncServiceResolver { + 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-device-debug-service.ts b/lib/services/android-device-debug-service.ts index b079862778..bb1bf5c5e1 100644 --- a/lib/services/android-device-debug-service.ts +++ b/lib/services/android-device-debug-service.ts @@ -13,9 +13,11 @@ export class AndroidDeviceDebugService extends DebugServiceBase implements IDevi constructor(protected device: Mobile.IAndroidDevice, protected $devicesService: Mobile.IDevicesService, + protected $cleanupService: ICleanupService, private $errors: IErrors, private $logger: ILogger, private $androidProcessService: Mobile.IAndroidProcessService, + private $staticConfig: IStaticConfig, private $net: INet, private $deviceLogProvider: Mobile.IDeviceLogProvider) { @@ -69,8 +71,7 @@ export class AndroidDeviceDebugService extends DebugServiceBase implements IDevi await this.unixSocketForward(port, `${unixSocketName}`); } - // TODO: Uncomment for 6.0.0 release - // await this.$cleanupService.addCleanupCommand({ command: await this.$staticConfig.getAdbFilePath(), args: ["-s", deviceId, "forward", "--remove", `tcp:${port}`] }); + await this.$cleanupService.addCleanupCommand({ command: await this.$staticConfig.getAdbFilePath(), args: ["-s", deviceId, "forward", "--remove", `tcp:${port}`] }); return port; } 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 d1565056a3..e6e8c362e6 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -4,37 +4,29 @@ import * as constants from "../constants"; import * as semver from "semver"; import * as projectServiceBaseLib from "./platform-project-service-base"; import { DeviceAndroidDebugBridge } from "../common/mobile/android/device-android-debug-bridge"; -import { attachAwaitDetach, isRecommendedAarFile } from "../common/helpers"; import { Configurations, LiveSyncPaths } from "../common/constants"; -import { SpawnOptions } from "child_process"; 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"; private static MIN_RUNTIME_VERSION_WITH_GRADLE = "1.5.0"; - private static MIN_RUNTIME_VERSION_WITHOUT_DEPS = "4.2.0-2018-06-29-02"; - - private isAndroidStudioTemplate: boolean; constructor(private $androidToolsInfo: IAndroidToolsInfo, - private $childProcess: IChildProcess, private $errors: IErrors, $fs: IFileSystem, - private $hostInfo: IHostInfo, private $logger: ILogger, $projectDataService: IProjectDataService, private $injector: IInjector, - private $pluginVariablesService: IPluginVariablesService, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - private $packageManager: INodePackageManager, private $androidPluginBuildService: IAndroidPluginBuildService, private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, private $androidResourcesMigrationService: IAndroidResourcesMigrationService, - private $filesHashService: IFilesHashService) { + private $filesHashService: IFilesHashService, + private $gradleCommandService: IGradleCommandService, + private $gradleBuildService: IGradleBuildService) { super($fs, $projectDataService); - this.isAndroidStudioTemplate = false; } private _platformData: IPlatformData = null; @@ -44,38 +36,27 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } if (projectData && projectData.platformsDir) { const projectRoot = path.join(projectData.platformsDir, AndroidProjectService.ANDROID_PLATFORM_NAME); - if (this.isAndroidStudioCompatibleTemplate(projectData)) { - this.isAndroidStudioTemplate = true; - } - - const appDestinationDirectoryArr = [projectRoot]; - if (this.isAndroidStudioTemplate) { - appDestinationDirectoryArr.push(constants.APP_FOLDER_NAME); - } - appDestinationDirectoryArr.push(constants.SRC_DIR, constants.MAIN_DIR, constants.ASSETS_DIR); - const configurationsDirectoryArr = [projectRoot]; - if (this.isAndroidStudioTemplate) { - configurationsDirectoryArr.push(constants.APP_FOLDER_NAME); - } - configurationsDirectoryArr.push(constants.SRC_DIR, constants.MAIN_DIR, constants.MANIFEST_FILE_NAME); - - const deviceBuildOutputArr = [projectRoot]; - if (this.isAndroidStudioTemplate) { - deviceBuildOutputArr.push(constants.APP_FOLDER_NAME); - } - deviceBuildOutputArr.push(constants.BUILD_DIR, constants.OUTPUTS_DIR, constants.APK_DIR); + const appDestinationDirectoryArr = [projectRoot, constants.APP_FOLDER_NAME, constants.SRC_DIR, constants.MAIN_DIR, constants.ASSETS_DIR]; + const configurationsDirectoryArr = [projectRoot, constants.APP_FOLDER_NAME, constants.SRC_DIR, constants.MAIN_DIR, constants.MANIFEST_FILE_NAME]; + const deviceBuildOutputArr = [projectRoot, constants.APP_FOLDER_NAME, constants.BUILD_DIR, constants.OUTPUTS_DIR, constants.APK_DIR]; const packageName = this.getProjectNameFromId(projectData); 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(); @@ -148,9 +129,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject }; } - public async validatePlugins(): Promise { /* */ } - - 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,78 +139,9 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject const targetSdkVersion = androidToolsInfo && androidToolsInfo.targetSdkVersion; this.$logger.trace(`Using Android SDK '${targetSdkVersion}'.`); - this.isAndroidStudioTemplate = this.isAndroidStudioCompatibleTemplate(projectData, frameworkVersion); - if (this.isAndroidStudioTemplate) { - this.copy(this.getPlatformData(projectData).projectRoot, frameworkDir, "*", "-R"); - } else { - this.copy(this.getPlatformData(projectData).projectRoot, frameworkDir, "libs", "-R"); - - if (config.pathToTemplate) { - const mainPath = path.join(this.getPlatformData(projectData).projectRoot, constants.SRC_DIR, constants.MAIN_DIR); - this.$fs.createDirectory(mainPath); - shell.cp("-R", path.join(path.resolve(config.pathToTemplate), "*"), mainPath); - } else { - this.copy(this.getPlatformData(projectData).projectRoot, frameworkDir, constants.SRC_DIR, "-R"); - } - this.copy(this.getPlatformData(projectData).projectRoot, frameworkDir, "build.gradle settings.gradle build-tools", "-Rf"); - - try { - this.copy(this.getPlatformData(projectData).projectRoot, frameworkDir, "gradle.properties", "-Rf"); - } catch (e) { - this.$logger.warn(`\n${e}\nIt's possible, the final .apk file will contain all architectures instead of the ones described in the abiFilters!\nYou can fix this by using the latest android platform.`); - } - - this.copy(this.getPlatformData(projectData).projectRoot, frameworkDir, "gradle", "-R"); - this.copy(this.getPlatformData(projectData).projectRoot, frameworkDir, "gradlew gradlew.bat", "-f"); - } + this.copy(this.getPlatformData(projectData).projectRoot, frameworkDir, "*", "-R"); this.cleanResValues(targetSdkVersion, projectData); - - if (semver.lt(frameworkVersion, AndroidProjectService.MIN_RUNTIME_VERSION_WITHOUT_DEPS)) { - await this.installRuntimeDeps(projectData, config); - } - } - - private async installRuntimeDeps(projectData: IProjectData, config: ICreateProjectOptions) { - const requiredDevDependencies = [ - { name: "babel-traverse", version: "^6.4.5" }, - { name: "babel-types", version: "^6.4.5" }, - { name: "babylon", version: "^6.4.5" }, - { name: "lazy", version: "^1.0.11" } - ]; - - const npmConfig: INodePackageManagerInstallOptions = { - save: true, - "save-dev": true, - "save-exact": true, - silent: true, - disableNpmInstall: false, - frameworkPath: config.frameworkPath, - ignoreScripts: config.ignoreScripts - }; - - const projectPackageJson: any = this.$fs.readJson(projectData.projectFilePath); - - for (const dependency of requiredDevDependencies) { - let dependencyVersionInProject = (projectPackageJson.dependencies && projectPackageJson.dependencies[dependency.name]) || - (projectPackageJson.devDependencies && projectPackageJson.devDependencies[dependency.name]); - - if (!dependencyVersionInProject) { - await this.$packageManager.install(`${dependency.name}@${dependency.version}`, projectData.projectDir, npmConfig); - } else { - const cleanedVersion = semver.clean(dependencyVersionInProject); - - // The plugin version is not valid. Check node_modules for the valid version. - if (!cleanedVersion) { - const pathToPluginPackageJson = path.join(projectData.projectDir, constants.NODE_MODULES_FOLDER_NAME, dependency.name, constants.PACKAGE_JSON_FILE_NAME); - dependencyVersionInProject = this.$fs.exists(pathToPluginPackageJson) && this.$fs.readJson(pathToPluginPackageJson).version; - } - - if (!semver.satisfies(dependencyVersionInProject || cleanedVersion, dependency.version)) { - this.$errors.failWithoutHelp(`Your project have installed ${dependency.name} version ${cleanedVersion} but Android platform requires version ${dependency.version}.`); - } - } - } } private cleanResValues(targetSdkVersion: number, projectData: IProjectData): void { @@ -256,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; @@ -288,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 { @@ -327,79 +233,16 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } @performanceLog() - public async buildProject(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig): Promise { - let task; - const gradleArgs = this.getGradleBuildOptions(buildConfig, projectData); - const baseTask = buildConfig.androidBundle ? "bundle" : "assemble"; + public async buildProject(projectRoot: string, projectData: IProjectData, buildData: IAndroidBuildData): Promise { const platformData = this.getPlatformData(projectData); - const outputPath = buildConfig.androidBundle ? platformData.bundleBuildOutputPath : platformData.getBuildOutputPath(buildConfig); - if (this.$logger.getLevel() === "TRACE") { - gradleArgs.unshift("--stacktrace"); - gradleArgs.unshift("--debug"); - } - if (buildConfig.release) { - task = `${baseTask}Release`; - } else { - task = `${baseTask}Debug`; - } - - gradleArgs.unshift(task); - - const handler = (data: any) => { - this.emit(constants.BUILD_OUTPUT_EVENT_NAME, data); - }; - - await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME, - this.$childProcess, - handler, - this.executeCommand({ - projectRoot: this.getPlatformData(projectData).projectRoot, - gradleArgs, - childProcessOpts: { stdio: buildConfig.buildOutputStdio || "inherit" }, - spawnFromEventOptions: { emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true }, - message: "Gradle build..." - }) - ); + await this.$gradleBuildService.buildProject(platformData.projectRoot, buildData); + const outputPath = platformData.getBuildOutputPath(buildData); await this.$filesHashService.saveHashesForProject(this._platformData, outputPath); } - private getGradleBuildOptions(settings: IAndroidBuildOptionsSettings, projectData: IProjectData): Array { - const configurationFilePath = this.getPlatformData(projectData).configurationFilePath; - - const buildOptions: Array = this.getBuildOptions(configurationFilePath); - - if (settings.release) { - buildOptions.push("-Prelease"); - buildOptions.push(`-PksPath=${path.resolve(settings.keyStorePath)}`); - buildOptions.push(`-Palias=${settings.keyStoreAlias}`); - buildOptions.push(`-Ppassword=${settings.keyStoreAliasPassword}`); - buildOptions.push(`-PksPassword=${settings.keyStorePassword}`); - } - - return buildOptions; - } - - private getBuildOptions(configurationFilePath?: string): Array { - this.$androidToolsInfo.validateInfo({ showWarningsAsErrors: true, validateTargetSdk: true }); - - const androidToolsInfo = this.$androidToolsInfo.getToolsInfo(); - const compileSdk = androidToolsInfo.compileSdkVersion; - const targetSdk = this.getTargetFromAndroidManifest(configurationFilePath) || compileSdk; - const buildToolsVersion = androidToolsInfo.buildToolsVersion; - const generateTypings = androidToolsInfo.generateTypings; - const buildOptions = [ - `-PcompileSdk=android-${compileSdk}`, - `-PtargetSdk=${targetSdk}`, - `-PbuildToolsVersion=${buildToolsVersion}`, - `-PgenerateTypings=${generateTypings}` - ]; - - return buildOptions; - } - - public async buildForDeploy(projectRoot: string, projectData: IProjectData, buildConfig?: IBuildConfig): Promise { - return this.buildProject(projectRoot, projectData, buildConfig); + public async buildForDeploy(projectRoot: string, projectData: IProjectData, buildData?: IAndroidBuildData): Promise { + return this.buildProject(projectRoot, projectData, buildData); } public isPlatformPrepared(projectRoot: string, projectData: IProjectData): boolean { @@ -437,171 +280,58 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } } - public prepareAppResources(appResourcesDirectoryPath: string, projectData: IProjectData): void { - this.cleanUpPreparedResources(appResourcesDirectoryPath, projectData); - } + public prepareAppResources(projectData: IProjectData): void { + const platformData = this.getPlatformData(projectData); + const projectAppResourcesPath = projectData.getAppResourcesDirectoryPath(projectData.projectDir); + const platformsAppResourcesPath = this.getAppResourcesDestinationDirectoryPath(projectData); - public async preparePluginNativeCode(pluginData: IPluginData, projectData: IProjectData): Promise { - if (!this.runtimeVersionIsGreaterThanOrEquals(projectData, "3.3.0")) { - const pluginPlatformsFolderPath = this.getPluginPlatformsFolderPath(pluginData, AndroidProjectService.ANDROID_PLATFORM_NAME); - await this.processResourcesFromPlugin(pluginData, pluginPlatformsFolderPath, projectData); - } else if (this.runtimeVersionIsGreaterThanOrEquals(projectData, "4.0.0")) { - // build Android plugins which contain AndroidManifest.xml and/or resources - const pluginPlatformsFolderPath = this.getPluginPlatformsFolderPath(pluginData, AndroidProjectService.ANDROID_PLATFORM_NAME); - if (this.$fs.exists(pluginPlatformsFolderPath)) { - const options: IPluginBuildOptions = { - projectDir: projectData.projectDir, - pluginName: pluginData.name, - platformsAndroidDirPath: pluginPlatformsFolderPath, - aarOutputDir: pluginPlatformsFolderPath, - tempPluginDirPath: path.join(projectData.platformsDir, "tempPlugin") - }; + this.cleanUpPreparedResources(projectAppResourcesPath, projectData); - await this.prebuildNativePlugin(options); - } - } + this.$fs.ensureDirectoryExists(platformsAppResourcesPath); - // Do nothing, the Android Gradle script will configure itself based on the input dependencies.json + const appResourcesDirStructureHasMigrated = this.$androidResourcesMigrationService.hasMigrated(projectAppResourcesPath); + if (appResourcesDirStructureHasMigrated) { + this.$fs.copyFile(path.join(projectAppResourcesPath, platformData.normalizedPlatformName, constants.SRC_DIR, "*"), platformsAppResourcesPath); + } else { + this.$fs.copyFile(path.join(projectAppResourcesPath, platformData.normalizedPlatformName, "*"), platformsAppResourcesPath); + // 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.deleteDirectory(path.join(platformsAppResourcesPath, "libs")); + } } - public async checkIfPluginsNeedBuild(projectData: IProjectData): Promise> { - const detectedPlugins: Array<{ platformsAndroidDirPath: string, pluginName: string }> = []; - - const platformsAndroid = path.join(constants.PLATFORMS_DIR_NAME, "android"); - const pathToPlatformsAndroid = path.join(projectData.projectDir, platformsAndroid); - const dependenciesJson = await this.$fs.readJson(path.join(pathToPlatformsAndroid, constants.DEPENDENCIES_JSON_NAME)); - const productionDependencies = dependenciesJson.map((item: any) => { - return path.resolve(pathToPlatformsAndroid, item.directory); - }); + public async preparePluginNativeCode(pluginData: IPluginData, projectData: IProjectData): Promise { + // build Android plugins which contain AndroidManifest.xml and/or resources + const pluginPlatformsFolderPath = this.getPluginPlatformsFolderPath(pluginData, AndroidProjectService.ANDROID_PLATFORM_NAME); + if (this.$fs.exists(pluginPlatformsFolderPath)) { + const options: IPluginBuildOptions = { + projectDir: projectData.projectDir, + pluginName: pluginData.name, + platformsAndroidDirPath: pluginPlatformsFolderPath, + aarOutputDir: pluginPlatformsFolderPath, + tempPluginDirPath: path.join(projectData.platformsDir, "tempPlugin") + }; - for (const dependency of productionDependencies) { - const jsonContent = this.$fs.readJson(path.join(dependency, constants.PACKAGE_JSON_FILE_NAME)); - const isPlugin = !!jsonContent.nativescript; - const pluginName = jsonContent.name; - if (isPlugin) { - const platformsAndroidDirPath = path.join(dependency, platformsAndroid); - if (this.$fs.exists(platformsAndroidDirPath)) { - let hasGeneratedAar = false; - let generatedAarPath = ""; - const nativeFiles = this.$fs.enumerateFilesInDirectorySync(platformsAndroidDirPath).filter((item) => { - if (isRecommendedAarFile(item, pluginName)) { - generatedAarPath = item; - hasGeneratedAar = true; - } - return this.isAllowedFile(item); - }); - - if (hasGeneratedAar) { - const aarStat = this.$fs.getFsStats(generatedAarPath); - nativeFiles.forEach((item) => { - const currentItemStat = this.$fs.getFsStats(item); - if (currentItemStat.mtime > aarStat.mtime) { - detectedPlugins.push({ - platformsAndroidDirPath, - pluginName - }); - } - }); - } else if (nativeFiles.length > 0) { - detectedPlugins.push({ - platformsAndroidDirPath, - pluginName - }); - } - } + if (await this.$androidPluginBuildService.buildAar(options)) { + this.$logger.info(`Built aar for ${options.pluginName}`); } - } - return detectedPlugins; - } - - private isAllowedFile(item: string): boolean { - return item.endsWith(constants.MANIFEST_FILE_NAME) || item.endsWith(constants.RESOURCES_DIR); - } - public async prebuildNativePlugin(options: IPluginBuildOptions): Promise { - if (await this.$androidPluginBuildService.buildAar(options)) { - this.$logger.info(`Built aar for ${options.pluginName}`); + this.$androidPluginBuildService.migrateIncludeGradle(options); } - - this.$androidPluginBuildService.migrateIncludeGradle(options); } public async processConfigurationFilesFromAppResources(): Promise { return; } - private async processResourcesFromPlugin(pluginData: IPluginData, pluginPlatformsFolderPath: string, projectData: IProjectData): Promise { - const configurationsDirectoryPath = path.join(this.getPlatformData(projectData).projectRoot, "configurations"); - this.$fs.ensureDirectoryExists(configurationsDirectoryPath); - - const pluginConfigurationDirectoryPath = path.join(configurationsDirectoryPath, pluginData.name); - if (this.$fs.exists(pluginPlatformsFolderPath)) { - this.$fs.ensureDirectoryExists(pluginConfigurationDirectoryPath); - - const isScoped = pluginData.name.indexOf("@") === 0; - const flattenedDependencyName = isScoped ? pluginData.name.replace("/", "_") : pluginData.name; - - // Copy all resources from plugin - const resourcesDestinationDirectoryPath = path.join(this.getPlatformData(projectData).projectRoot, constants.SRC_DIR, flattenedDependencyName); - this.$fs.ensureDirectoryExists(resourcesDestinationDirectoryPath); - shell.cp("-Rf", path.join(pluginPlatformsFolderPath, "*"), resourcesDestinationDirectoryPath); - - const filesForInterpolation = this.$fs.enumerateFilesInDirectorySync(resourcesDestinationDirectoryPath, file => this.$fs.getFsStats(file).isDirectory() || path.extname(file) === constants.XML_FILE_EXTENSION) || []; - for (const file of filesForInterpolation) { - this.$logger.trace(`Interpolate data for plugin file: ${file}`); - await this.$pluginVariablesService.interpolate(pluginData, file, projectData.projectDir, projectData.projectIdentifiers.android); - } - } - - // Copy include.gradle file - const includeGradleFilePath = path.join(pluginPlatformsFolderPath, constants.INCLUDE_GRADLE_NAME); - if (this.$fs.exists(includeGradleFilePath)) { - shell.cp("-f", includeGradleFilePath, pluginConfigurationDirectoryPath); - } - } - public async removePluginNativeCode(pluginData: IPluginData, projectData: IProjectData): Promise { - try { - if (!this.runtimeVersionIsGreaterThanOrEquals(projectData, "3.3.0")) { - const pluginConfigDir = path.join(this.getPlatformData(projectData).projectRoot, "configurations", pluginData.name); - if (this.$fs.exists(pluginConfigDir)) { - await this.cleanProject(this.getPlatformData(projectData).projectRoot, projectData); - } - } - } catch (e) { - if (e.code === "ENOENT") { - this.$logger.debug("No native code jars found: " + e.message); - } else { - throw e; - } - } + // not implemented } public async beforePrepareAllPlugins(projectData: IProjectData, dependencies?: IDependencyData[]): Promise { - const shouldUseNewRoutine = this.runtimeVersionIsGreaterThanOrEquals(projectData, "3.3.0"); - if (dependencies) { dependencies = this.filterUniqueDependencies(dependencies); - if (shouldUseNewRoutine) { - this.provideDependenciesJson(projectData, dependencies); - } else { - const platformDir = path.join(projectData.platformsDir, AndroidProjectService.ANDROID_PLATFORM_NAME); - const buildDir = path.join(platformDir, "build-tools"); - const checkV8dependants = path.join(buildDir, "check-v8-dependants.js"); - if (this.$fs.exists(checkV8dependants)) { - const stringifiedDependencies = JSON.stringify(dependencies); - try { - await this.spawn('node', [checkV8dependants, stringifiedDependencies, projectData.platformsDir], { stdio: "inherit" }); - } catch (e) { - this.$logger.info("Checking for dependants on v8 public API failed. This is likely caused because of cyclic production dependencies. Error code: " + e.code + "\nMore information: https://github.com/NativeScript/nativescript-cli/issues/2561"); - } - } - } - } - - if (!shouldUseNewRoutine) { - const projectRoot = this.getPlatformData(projectData).projectRoot; - await this.cleanProject(projectRoot, projectData); + this.provideDependenciesJson(projectData, dependencies); } } @@ -636,25 +366,18 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject return nativescript && (nativescript.android || (nativescript.platforms && nativescript.platforms.android)); } - public stopServices(projectRoot: string): Promise { - return this.executeCommand({ - projectRoot, - gradleArgs: ["--stop", "--quiet"], - childProcessOpts: { stdio: "pipe" }, - message: "Gradle stop services..." + public async stopServices(projectRoot: string): Promise { + const result = await this.$gradleCommandService.executeCommand(["--stop", "--quiet"], { + cwd: projectRoot, + message: "Gradle stop services...", + stdio: "pipe" }); + + return result; } - public async cleanProject(projectRoot: string, projectData: IProjectData): Promise { - if (this.$androidToolsInfo.getToolsInfo().androidHomeEnvVar) { - const gradleArgs = this.getGradleBuildOptions({ release: false }, projectData); - gradleArgs.unshift("clean"); - await this.executeCommand({ - projectRoot, - gradleArgs, - message: "Gradle clean..." - }); - } + public async cleanProject(projectRoot: string): Promise { + await this.$gradleBuildService.cleanProject(projectRoot, { release: false }); } public async cleanDeviceTempFolder(deviceIdentifier: string, projectData: IProjectData): Promise { @@ -663,7 +386,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. } @@ -674,10 +397,6 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject shell.cp(cpArg, paths, projectRoot); } - private async spawn(command: string, args: string[], opts?: any, spawnOpts?: ISpawnFromEventOptions): Promise { - return this.$childProcess.spawnFromEvent(command, args, "close", opts || { stdio: "inherit" }, spawnOpts); - } - private validatePackageName(packageName: string): void { //Make the package conform to Java package types //Enforce underscore limitation @@ -702,92 +421,14 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } } - private getTargetFromAndroidManifest(configurationFilePath: string): string { - let versionInManifest: string; - if (this.$fs.exists(configurationFilePath)) { - const targetFromAndroidManifest: string = this.$fs.readText(configurationFilePath); - if (targetFromAndroidManifest) { - const match = targetFromAndroidManifest.match(/.*?android:targetSdkVersion=\"(.*?)\"/); - if (match && match[1]) { - versionInManifest = match[1]; - } - } - } - - return versionInManifest; - } - - private async executeCommand(opts: { projectRoot: string, gradleArgs: any, childProcessOpts?: SpawnOptions, spawnFromEventOptions?: ISpawnFromEventOptions, message: string }): Promise { - if (this.$androidToolsInfo.getToolsInfo().androidHomeEnvVar) { - const { projectRoot, gradleArgs, message, spawnFromEventOptions } = opts; - const gradlew = this.$hostInfo.isWindows ? "gradlew.bat" : "./gradlew"; - - if (this.$logger.getLevel() === "INFO") { - gradleArgs.push("--quiet"); - } - - this.$logger.info(message); - - const childProcessOpts = opts.childProcessOpts || {}; - childProcessOpts.cwd = childProcessOpts.cwd || projectRoot; - childProcessOpts.stdio = childProcessOpts.stdio || "inherit"; - let commandResult; - try { - commandResult = await this.spawn(gradlew, - gradleArgs, - childProcessOpts, - spawnFromEventOptions); - } catch (err) { - this.$errors.failWithoutHelp(err.message); - } - - return commandResult; - } - } - - private isAndroidStudioCompatibleTemplate(projectData: IProjectData, frameworkVersion?: string): boolean { - const currentPlatformData: IDictionary = this.$projectDataService.getNSValue(projectData.projectDir, constants.TNS_ANDROID_RUNTIME_NAME); - const platformVersion = (currentPlatformData && currentPlatformData[constants.VERSION_STRING]) || frameworkVersion; - - if (!platformVersion) { - return true; - } - - if (!semver.valid(platformVersion)) { - return true; - } - - const androidStudioCompatibleTemplate = "3.4.0"; - const normalizedPlatformVersion = `${semver.major(platformVersion)}.${semver.minor(platformVersion)}.0`; - - return semver.gte(normalizedPlatformVersion, androidStudioCompatibleTemplate); - } - - private runtimeVersionIsGreaterThanOrEquals(projectData: IProjectData, versionString: string): boolean { - const platformVersion = this.getCurrentPlatformVersion(this.getPlatformData(projectData), projectData); - - if (platformVersion === constants.PackageVersion.NEXT) { - return true; - } - - const normalizedPlatformVersion = `${semver.major(platformVersion)}.${semver.minor(platformVersion)}.0`; - return semver.gte(normalizedPlatformVersion, versionString); - } - private getLegacyAppResourcesDestinationDirPath(projectData: IProjectData): string { - const resourcePath: string[] = [constants.SRC_DIR, constants.MAIN_DIR, constants.RESOURCES_DIR]; - if (this.isAndroidStudioTemplate) { - resourcePath.unshift(constants.APP_FOLDER_NAME); - } + const resourcePath: string[] = [constants.APP_FOLDER_NAME, constants.SRC_DIR, constants.MAIN_DIR, constants.RESOURCES_DIR]; return path.join(this.getPlatformData(projectData).projectRoot, ...resourcePath); } private getUpdatedAppResourcesDestinationDirPath(projectData: IProjectData): string { - const resourcePath: string[] = [constants.SRC_DIR]; - if (this.isAndroidStudioTemplate) { - resourcePath.unshift(constants.APP_FOLDER_NAME); - } + const resourcePath: string[] = [constants.APP_FOLDER_NAME, constants.SRC_DIR]; return path.join(this.getPlatformData(projectData).projectRoot, ...resourcePath); } diff --git a/lib/services/android/gradle-build-args-service.ts b/lib/services/android/gradle-build-args-service.ts new file mode 100644 index 0000000000..d2d50a8468 --- /dev/null +++ b/lib/services/android/gradle-build-args-service.ts @@ -0,0 +1,66 @@ +import * as path from "path"; +import { Configurations } from "../../common/constants"; + +export class GradleBuildArgsService implements IGradleBuildArgsService { + constructor(private $androidToolsInfo: IAndroidToolsInfo, + private $logger: ILogger) { } + + public getBuildTaskArgs(buildData: IAndroidBuildData): string[] { + const args = this.getBaseTaskArgs(buildData); + args.unshift(this.getBuildTaskName(buildData)); + + return args; + } + + public getCleanTaskArgs(buildData: IAndroidBuildData): string[] { + const args = this.getBaseTaskArgs(buildData); + args.unshift("clean"); + + return args; + } + + private getBaseTaskArgs(buildData: IAndroidBuildData): string[] { + const args = this.getBuildLoggingArgs(); + + const toolsInfo = this.$androidToolsInfo.getToolsInfo(); + args.push( + `-PcompileSdk=android-${toolsInfo.compileSdkVersion}`, + `-PtargetSdk=${toolsInfo.targetSdkVersion}`, + `-PbuildToolsVersion=${toolsInfo.buildToolsVersion}`, + `-PgenerateTypings=${toolsInfo.generateTypings}` + ); + + if (buildData.release) { + args.push( + "-Prelease", + `-PksPath=${path.resolve(buildData.keyStorePath)}`, + `-Palias=${buildData.keyStoreAlias}`, + `-Ppassword=${buildData.keyStoreAliasPassword}`, + `-PksPassword=${buildData.keyStorePassword}` + ); + } + + return args; + } + + private getBuildLoggingArgs(): string[] { + const args = []; + + const logLevel = this.$logger.getLevel(); + if (logLevel === "TRACE") { + args.push("--stacktrace", "--debug"); + } else if (logLevel === "INFO") { + args.push("--quiet"); + } + + return args; + } + + private getBuildTaskName(buildData: IAndroidBuildData): string { + const baseTaskName = buildData.androidBundle ? "bundle" : "assemble"; + const buildTaskName = buildData.release ? `${baseTaskName}${Configurations.Release}` : `${baseTaskName}${Configurations.Debug}`; + + return buildTaskName; + } +} +$injector.register("gradleBuildArgsService", GradleBuildArgsService); diff --git a/lib/services/android/gradle-build-service.ts b/lib/services/android/gradle-build-service.ts new file mode 100644 index 0000000000..1f34367b5c --- /dev/null +++ b/lib/services/android/gradle-build-service.ts @@ -0,0 +1,30 @@ +import { attachAwaitDetach } from "../../common/helpers"; +import * as constants from "../../constants"; +import { EventEmitter } from "events"; + +export class GradleBuildService extends EventEmitter implements IGradleBuildService { + constructor( + private $childProcess: IChildProcess, + private $gradleBuildArgsService: IGradleBuildArgsService, + private $gradleCommandService: IGradleCommandService, + ) { super(); } + + public async buildProject(projectRoot: string, buildData: IAndroidBuildData): Promise { + const buildTaskArgs = this.$gradleBuildArgsService.getBuildTaskArgs(buildData); + const spawnOptions = { emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true }; + const gradleCommandOptions = { cwd: projectRoot, message: "Gradle build...", stdio: buildData.buildOutputStdio, spawnOptions }; + + await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME, + this.$childProcess, + (data: any) => this.emit(constants.BUILD_OUTPUT_EVENT_NAME, data), + this.$gradleCommandService.executeCommand(buildTaskArgs, gradleCommandOptions) + ); + } + + public async cleanProject(projectRoot: string, buildData: IAndroidBuildData): Promise { + const cleanTaskArgs = this.$gradleBuildArgsService.getCleanTaskArgs(buildData); + const gradleCommandOptions = { cwd: projectRoot, message: "Gradle clean..." }; + await this.$gradleCommandService.executeCommand(cleanTaskArgs, gradleCommandOptions); + } +} +$injector.register("gradleBuildService", GradleBuildService); diff --git a/lib/services/android/gradle-command-service.ts b/lib/services/android/gradle-command-service.ts new file mode 100644 index 0000000000..07e398450b --- /dev/null +++ b/lib/services/android/gradle-command-service.ts @@ -0,0 +1,31 @@ +export class GradleCommandService implements IGradleCommandService { + constructor( + private $childProcess: IChildProcess, + private $errors: IErrors, + private $hostInfo: IHostInfo, + private $logger: ILogger + ) { } + + public async executeCommand(gradleArgs: string[], options: IGradleCommandOptions): Promise { + const { message, cwd, stdio, spawnOptions } = options; + this.$logger.info(message); + + const childProcessOptions = { cwd, stdio: stdio || "inherit" }; + const gradleExecutable = this.$hostInfo.isWindows ? "gradlew.bat" : "./gradlew"; + + const result = await this.executeCommandSafe(gradleExecutable, gradleArgs, childProcessOptions, spawnOptions); + + return result; + } + + private async executeCommandSafe(gradleExecutable: string, gradleArgs: string[], childProcessOptions: { cwd: string, stdio: string }, spawnOptions: ISpawnFromEventOptions): Promise { + try { + const result = await this.$childProcess.spawnFromEvent(gradleExecutable, gradleArgs, "close", childProcessOptions, spawnOptions); + + return result; + } catch (err) { + this.$errors.failWithoutHelp(err.message); + } + } +} +$injector.register("gradleCommandService", GradleCommandService); 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..a05f083d82 --- /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 getLatestAppPackagePath(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 getAllAppPackages(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 copyLatestAppPackage(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.getAllAppPackages(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..abb8e5fd34 --- /dev/null +++ b/lib/services/build-info-file-service.ts @@ -0,0 +1,69 @@ +import * as path from "path"; +import * as helpers from "../common/helpers"; + +const buildInfoFileName = ".nsbuildinfo"; + +export class BuildInfoFileService implements IBuildInfoFileService { + constructor( + private $devicePathProvider: IDevicePathProvider, + private $fs: IFileSystem, + private $mobileHelper: Mobile.IMobileHelper, + private $projectChangesService: IProjectChangesService + ) { } + + public getLocalBuildInfo(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; + } + + public 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; + } + } + + public saveLocalBuildInfo(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 async saveDeviceBuildInfo(device: Mobile.IDevice, projectData: IProjectData, outputFilePath: string): Promise { + const deviceFilePath = await this.getDeviceBuildInfoFilePath(device, projectData); + const appIdentifier = projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()]; + + await device.fileSystem.putFile(path.join(outputFilePath, buildInfoFileName), deviceFilePath, appIdentifier); + } + + 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 + }); + const result = helpers.fromWindowsRelativePathToUnix(path.join(deviceRootPath, buildInfoFileName)); + + return result; + } +} +$injector.register("buildInfoFileService", BuildInfoFileService); diff --git a/lib/services/debug-data-service.ts b/lib/services/debug-data-service.ts index 77ae6705f8..910799d986 100644 --- a/lib/services/debug-data-service.ts +++ b/lib/services/debug-data-service.ts @@ -2,14 +2,16 @@ export class DebugDataService implements IDebugDataService { constructor( private $devicesService: Mobile.IDevicesService ) { } - public createDebugData(projectData: IProjectData, options: IDeviceIdentifier): IDebugData { - const device = this.$devicesService.getDeviceByIdentifier(options.device); + + public getDebugData(deviceIdentifier: string, projectData: IProjectData, debugOptions: IDebugOptions): IDebugData { + const device = this.$devicesService.getDeviceByIdentifier(deviceIdentifier); return { applicationIdentifier: projectData.projectIdentifiers[device.deviceInfo.platform.toLowerCase()], projectDir: projectData.projectDir, - deviceIdentifier: options.device, - projectName: projectData.projectName + deviceIdentifier, + projectName: projectData.projectName, + debugOptions }; } } diff --git a/lib/services/debug-service.ts b/lib/services/debug-service.ts deleted file mode 100644 index 3958df4f79..0000000000 --- a/lib/services/debug-service.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { platform } from "os"; -import { parse } from "url"; -import { EventEmitter } from "events"; -import { CONNECTION_ERROR_EVENT_NAME, DebugCommandErrors } from "../constants"; -import { CONNECTED_STATUS } from "../common/constants"; -import { DebugTools, TrackActionNames } from "../constants"; -import { performanceLog } from "../common/decorators"; - -export class DebugService extends EventEmitter implements IDebugService { - private _platformDebugServices: IDictionary; - constructor(private $devicesService: Mobile.IDevicesService, - private $errors: IErrors, - private $injector: IInjector, - private $hostInfo: IHostInfo, - private $mobileHelper: Mobile.IMobileHelper, - private $analyticsService: IAnalyticsService) { - super(); - this._platformDebugServices = {}; - } - - @performanceLog() - public async debug(debugData: IDebugData, options: IDebugOptions): Promise { - const device = this.$devicesService.getDeviceByIdentifier(debugData.deviceIdentifier); - - if (!device) { - this.$errors.failWithoutHelp(`Cannot find device with identifier ${debugData.deviceIdentifier}.`); - } - - if (device.deviceInfo.status !== CONNECTED_STATUS) { - this.$errors.failWithoutHelp(`The device with identifier ${debugData.deviceIdentifier} is unreachable. Make sure it is Trusted and try again.`); - } - - await this.$analyticsService.trackEventActionInGoogleAnalytics({ - action: TrackActionNames.Debug, - device, - additionalData: this.$mobileHelper.isiOSPlatform(device.deviceInfo.platform) && options && options.inspector ? DebugTools.Inspector : DebugTools.Chrome, - projectDir: debugData.projectDir - }); - - if (!(await device.applicationManager.isApplicationInstalled(debugData.applicationIdentifier))) { - this.$errors.failWithoutHelp(`The application ${debugData.applicationIdentifier} is not installed on device with identifier ${debugData.deviceIdentifier}.`); - } - - const debugOptions: IDebugOptions = _.cloneDeep(options); - const debugService = this.getDeviceDebugService(device); - if (!debugService) { - this.$errors.failWithoutHelp(`Unsupported device OS: ${device.deviceInfo.platform}. You can debug your applications only on iOS or Android.`); - } - - // 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) { - this.$errors.failWithoutHelp("To debug on iOS simulator you need to provide path to the app package."); - } - - if (!this.$hostInfo.isWindows && !this.$hostInfo.isDarwin) { - this.$errors.failWithoutHelp(`Debugging on iOS devices is not supported for ${platform()} yet.`); - } - } - - const debugResultInfo = await debugService.debug(debugData, debugOptions); - - return this.getDebugInformation(debugResultInfo, device.deviceInfo.identifier); - } - - public debugStop(deviceIdentifier: string): Promise { - const debugService = this.getDeviceDebugServiceByIdentifier(deviceIdentifier); - return debugService.debugStop(); - } - - protected getDeviceDebugService(device: Mobile.IDevice): IDeviceDebugService { - if (!this._platformDebugServices[device.deviceInfo.identifier]) { - const devicePlatform = device.deviceInfo.platform; - if (this.$mobileHelper.isiOSPlatform(devicePlatform)) { - this._platformDebugServices[device.deviceInfo.identifier] = this.$injector.resolve("iOSDeviceDebugService", { device }); - } else if (this.$mobileHelper.isAndroidPlatform(devicePlatform)) { - this._platformDebugServices[device.deviceInfo.identifier] = this.$injector.resolve("androidDeviceDebugService", { device }); - } else { - this.$errors.failWithoutHelp(DebugCommandErrors.UNSUPPORTED_DEVICE_OS_FOR_DEBUGGING); - } - - this.attachConnectionErrorHandlers(this._platformDebugServices[device.deviceInfo.identifier]); - } - - return this._platformDebugServices[device.deviceInfo.identifier]; - } - - private getDeviceDebugServiceByIdentifier(deviceIdentifier: string): IDeviceDebugService { - const device = this.$devicesService.getDeviceByIdentifier(deviceIdentifier); - return this.getDeviceDebugService(device); - } - - private attachConnectionErrorHandlers(platformDebugService: IDeviceDebugService) { - let connectionErrorHandler = (e: Error) => this.emit(CONNECTION_ERROR_EVENT_NAME, e); - connectionErrorHandler = connectionErrorHandler.bind(this); - platformDebugService.on(CONNECTION_ERROR_EVENT_NAME, connectionErrorHandler); - } - - private getDebugInformation(debugResultInfo: IDebugResultInfo, deviceIdentifier: string): IDebugInformation { - const debugInfo: IDebugInformation = { - url: debugResultInfo.debugUrl, - port: 0, - deviceIdentifier - }; - - if (debugResultInfo.debugUrl) { - const parseQueryString = true; - const wsQueryParam = parse(debugResultInfo.debugUrl, parseQueryString).query.ws; - const hostPortSplit = wsQueryParam && wsQueryParam.split(":"); - debugInfo.port = hostPortSplit && +hostPortSplit[1]; - } - - return debugInfo; - } -} - -$injector.register("debugService", DebugService); 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..02fe9ecd71 --- /dev/null +++ b/lib/services/device/device-install-app-service.ts @@ -0,0 +1,91 @@ +import { TrackActionNames, HASHES_FILE_NAME } from "../../constants"; +import * as path from "path"; + +export class DeviceInstallAppService { + constructor( + private $analyticsService: IAnalyticsService, + private $buildArtefactsService: IBuildArtefactsService, + private $buildInfoFileService: IBuildInfoFileService, + private $fs: IFileSystem, + private $logger: ILogger, + private $mobileHelper: Mobile.IMobileHelper, + private $projectDataService: IProjectDataService, + private $platformsDataService: IPlatformsDataService + ) { } + + public async installOnDevice(device: Mobile.IDevice, buildData: IBuildData, packageFile?: string): Promise { + this.$logger.info(`Installing on device ${device.deviceInfo.identifier}...`); + + const platform = device.deviceInfo.platform.toLowerCase(); + const projectData = this.$projectDataService.getProjectData(buildData.projectDir); + const platformData = this.$platformsDataService.getPlatformData(platform, projectData); + + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: TrackActionNames.Deploy, + device, + projectDir: projectData.projectDir + }); + + if (!packageFile) { + packageFile = await this.$buildArtefactsService.getLatestAppPackagePath(platformData, buildData); + } + + await platformData.platformProjectService.cleanDeviceTempFolder(device.deviceInfo.identifier, projectData); + + const appIdentifier = projectData.projectIdentifiers[platform]; + const outputFilePath = buildData.outputPath || platformData.getBuildOutputPath(buildData); + + await device.applicationManager.reinstallApplication(appIdentifier, packageFile); + + await this.updateHashesOnDevice({ + device, + appIdentifier, + outputFilePath, + platformData + }); + + if (!buildData.release) { + await this.$buildInfoFileService.saveDeviceBuildInfo(device, projectData, outputFilePath); + } + + this.$logger.info(`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 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.$buildInfoFileService.getDeviceBuildInfo(device, projectData); + const localBuildInfo = this.$buildInfoFileService.getLocalBuildInfo(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, HASHES_FILE_NAME); + if (this.$fs.exists(hashesFilePath)) { + hashes = this.$fs.readJson(hashesFilePath); + } + + await device.fileSystem.updateHashesOnDevice(hashes, appIdentifier); + } +} +$injector.register("deviceInstallAppService", DeviceInstallAppService); 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/ios-device-debug-service.ts b/lib/services/ios-device-debug-service.ts index 16b34c3207..1205d7a50d 100644 --- a/lib/services/ios-device-debug-service.ts +++ b/lib/services/ios-device-debug-service.ts @@ -6,6 +6,7 @@ const inspectorAppName = "NativeScript Inspector.app"; const inspectorNpmPackageName = "tns-ios-inspector"; const inspectorUiDir = "WebInspectorUI/"; import { performanceLog } from "../common/decorators"; +import { platform } from "os"; export class IOSDeviceDebugService extends DebugServiceBase implements IDeviceDebugService { private deviceIdentifier: string; @@ -44,6 +45,10 @@ export class IOSDeviceDebugService extends DebugServiceBase implements IDeviceDe } private validateOptions(debugOptions: IDebugOptions) { + if (!this.$hostInfo.isWindows && !this.$hostInfo.isDarwin) { + this.$errors.failWithoutHelp(`Debugging on iOS devices is not supported for ${platform()} yet.`); + } + if (debugOptions.debugBrk && debugOptions.start) { this.$errors.failWithoutHelp("Expected exactly one of the --debug-brk or --start options."); } diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 2fcf6e9144..149fec0188 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -1,6 +1,5 @@ import * as path from "path"; import * as shell from "shelljs"; -import * as semver from "semver"; import * as constants from "../constants"; import { Configurations } from "../common/constants"; import * as helpers from "../common/helpers"; @@ -12,8 +11,9 @@ import * as temp from "temp"; import * as plist from "plist"; import { IOSProvisionService } from "./ios-provision-service"; import { IOSEntitlementsService } from "./ios-entitlements-service"; -import * as mobileProvisionFinder from "ios-mobileprovision-finder"; -import { BUILD_XCCONFIG_FILE_NAME, IosProjectConstants, IOSNativeTargetProductTypes } from "../constants"; +import { IOSBuildData } from "../data/build-data"; +import { IOSPrepareData } from "../data/prepare-data"; +import { BUILD_XCCONFIG_FILE_NAME, IosProjectConstants } from "../constants"; interface INativeSourceCodeGroup { name: string; @@ -21,18 +21,13 @@ interface INativeSourceCodeGroup { files: string[]; } -enum ProductArgs { - target = "target", - scheme = "scheme" -} - const DevicePlatformSdkName = "iphoneos"; 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"; @@ -43,21 +38,18 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ private $logger: ILogger, private $injector: IInjector, $projectDataService: IProjectDataService, - private $prompter: IPrompter, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - private $devicesService: Mobile.IDevicesService, - private $mobileHelper: Mobile.IMobileHelper, private $hostInfo: IHostInfo, - private $pluginVariablesService: IPluginVariablesService, private $xcprojService: IXcprojService, private $iOSProvisionService: IOSProvisionService, + private $iOSSigningService: IiOSSigningService, private $pbxprojDomXcode: IPbxprojDomXcode, private $xcode: IXcode, private $iOSEntitlementsService: IOSEntitlementsService, private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, private $plistParser: IPlistParser, - private $sysInfo: ISysInfo, private $xcconfigService: IXcconfigService, + private $xcodebuildService: IXcodebuildService, private $iOSExtensionsService: IIOSExtensionsService, private $iOSWatchAppService: IIOSWatchAppService) { super($fs, $projectDataService); @@ -76,8 +68,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); @@ -117,25 +110,17 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ if (provision === true) { await this.$iOSProvisionService.listProvisions(projectId); this.$errors.failWithoutHelp("Please provide provisioning profile uuid or name with the --provision option."); - return false; } if (teamId === true) { await this.$iOSProvisionService.listTeams(); this.$errors.failWithoutHelp("Please provide team id or team name with the --teamId options."); - return false; } return true; } public getAppResourcesDestinationDirectoryPath(projectData: IProjectData): string { - const frameworkVersion = this.getFrameworkVersion(projectData); - - if (semver.lt(frameworkVersion, "1.3.0")) { - return path.join(this.getPlatformData(projectData).projectRoot, projectData.projectName, "Resources", "icons"); - } - return path.join(this.getPlatformData(projectData).projectRoot, projectData.projectName, "Resources"); } @@ -156,23 +141,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. @@ -201,433 +176,47 @@ 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)); } - /** - * Archive the Xcode project to .xcarchive. - * Returns the path to the .xcarchive. - */ - public async archive(projectData: IProjectData, buildConfig?: IBuildConfig, options?: { archivePath?: string, additionalArgs?: string[] }): Promise { + public async buildProject(projectRoot: string, projectData: IProjectData, iOSBuildData: IOSBuildData): Promise { const platformData = this.getPlatformData(projectData); - const projectRoot = this.getPlatformData(projectData).projectRoot; - const archivePath = options && options.archivePath ? path.resolve(options.archivePath) : path.join(platformData.getBuildOutputPath(buildConfig), projectData.projectName + ".xcarchive"); - let args = ["archive", "-archivePath", archivePath, "-configuration", - getConfigurationName(!buildConfig || buildConfig.release)] - .concat(this.xcbuildProjectArgs(projectRoot, projectData, ProductArgs.scheme)); - - if (options && options.additionalArgs) { - args = args.concat(options.additionalArgs); - } - - await this.xcodebuild(args, projectRoot, buildConfig && buildConfig.buildOutputStdio); - return archivePath; - } - - /** - * Exports .xcarchive for AppStore distribution. - */ - public async exportArchive(projectData: IProjectData, options: { archivePath: string, exportDir?: string, teamID?: string, provision?: string }): Promise { - const projectRoot = this.getPlatformData(projectData).projectRoot; - const archivePath = options.archivePath; - // The xcodebuild exportPath expects directory and writes the .ipa at that directory. - const exportPath = path.resolve(options.exportDir || path.join(projectRoot, "/build/archive")); - const exportFile = path.join(exportPath, projectData.projectName + ".ipa"); - - // These are the options that you can set in the Xcode UI when exporting for AppStore deployment. - let plistTemplate = ` - - - -`; - if (options && options.teamID) { - plistTemplate += ` teamID - ${options.teamID} -`; - } - if (options && options.provision) { - plistTemplate += ` provisioningProfiles - - ${projectData.projectIdentifiers.ios} - ${options.provision} - `; - } - plistTemplate += ` method - app-store - uploadBitcode - - compileBitcode - - uploadSymbols - - -`; - - // Save the options... - temp.track(); - const exportOptionsPlist = temp.path({ prefix: "export-", suffix: ".plist" }); - this.$fs.writeFile(exportOptionsPlist, plistTemplate); - - await this.xcodebuild( - [ - "-exportArchive", - "-archivePath", archivePath, - "-exportPath", exportPath, - "-exportOptionsPlist", exportOptionsPlist - ], - projectRoot); - return exportFile; - } - - private iCloudContainerEnvironment(buildConfig: IBuildConfig): string { - return buildConfig && buildConfig.iCloudContainerEnvironment ? buildConfig.iCloudContainerEnvironment : null; - } - - /** - * Exports .xcarchive for a development device. - */ - private async exportDevelopmentArchive(projectData: IProjectData, buildConfig: IBuildConfig, options: { archivePath: string, provision?: string }): Promise { - const platformData = this.getPlatformData(projectData); - const projectRoot = platformData.projectRoot; - const archivePath = options.archivePath; - const exportOptionsMethod = await this.getExportOptionsMethod(projectData, archivePath); - const iCloudContainerEnvironment = this.iCloudContainerEnvironment(buildConfig); - let plistTemplate = ` - - - - method - ${exportOptionsMethod}`; - if (options && options.provision) { - plistTemplate += ` provisioningProfiles - - ${projectData.projectIdentifiers.ios} - ${options.provision} -`; - } - plistTemplate += ` - uploadBitcode - - compileBitcode - `; - if (iCloudContainerEnvironment) { - plistTemplate += ` - iCloudContainerEnvironment - ${iCloudContainerEnvironment}`; - } - plistTemplate += ` - -`; - - // Save the options... - temp.track(); - const exportOptionsPlist = temp.path({ prefix: "export-", suffix: ".plist" }); - this.$fs.writeFile(exportOptionsPlist, plistTemplate); - - // The xcodebuild exportPath expects directory and writes the .ipa at that directory. - const exportPath = path.resolve(path.dirname(archivePath)); - const exportFile = path.join(exportPath, projectData.projectName + ".ipa"); - - await this.xcodebuild( - [ - "-exportArchive", - "-archivePath", archivePath, - "-exportPath", exportPath, - "-exportOptionsPlist", exportOptionsPlist - ], - projectRoot, buildConfig.buildOutputStdio); - return exportFile; - } - - private xcbuildProjectArgs(projectRoot: string, projectData: IProjectData, product?: ProductArgs): string[] { - const xcworkspacePath = path.join(projectRoot, projectData.projectName + ".xcworkspace"); - if (this.$fs.exists(xcworkspacePath)) { - return ["-workspace", xcworkspacePath, product ? "-" + product : "-scheme", projectData.projectName]; - } else { - const xcodeprojPath = path.join(projectRoot, projectData.projectName + ".xcodeproj"); - return ["-project", xcodeprojPath, product ? "-" + product : "-target", projectData.projectName]; - } - } - - public async buildProject(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig): Promise { - const basicArgs = [ - 'SHARED_PRECOMPS_DIR=' + path.join(projectRoot, 'build', 'sharedpch') - ]; - - // Starting from tns-ios 1.4 the xcconfig file is referenced in the project template - const frameworkVersion = this.getFrameworkVersion(projectData); - if (semver.lt(frameworkVersion, "1.4.0")) { - basicArgs.push("-xcconfig", path.join(projectRoot, projectData.projectName, BUILD_XCCONFIG_FILE_NAME)); - } const handler = (data: any) => { this.emit(constants.BUILD_OUTPUT_EVENT_NAME, data); }; - if (buildConfig.buildForDevice) { + if (iOSBuildData.buildForDevice) { + await this.$iOSSigningService.setupSigningForDevice(projectRoot, projectData, iOSBuildData); await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME, this.$childProcess, handler, - this.buildForDevice(projectRoot, basicArgs, buildConfig, projectData)); + this.$xcodebuildService.buildForDevice(platformData, projectData, iOSBuildData)); + } else if (iOSBuildData.buildForAppStore) { + await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME, + this.$childProcess, + handler, + this.$xcodebuildService.buildForAppStore(platformData, projectData, iOSBuildData)); } else { await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME, this.$childProcess, handler, - this.buildForSimulator(projectRoot, basicArgs, projectData, buildConfig)); + this.$xcodebuildService.buildForSimulator(platformData, projectData, iOSBuildData)); } this.validateApplicationIdentifier(projectData); } - public async validatePlugins(projectData: IProjectData): Promise { - const installedPlugins = await (this.$injector.resolve("pluginsService")).getAllInstalledPlugins(projectData); - for (const pluginData of installedPlugins) { - const pluginsFolderExists = this.$fs.exists(path.join(pluginData.pluginPlatformsFolderPath(this.$devicePlatformsConstants.iOS.toLowerCase()), "Podfile")); - const cocoaPodVersion = await this.$sysInfo.getCocoaPodsVersion(); - if (pluginsFolderExists && !cocoaPodVersion) { - this.$errors.failWithoutHelp(`${pluginData.name} has Podfile and you don't have Cocoapods installed or it is not configured correctly. Please verify Cocoapods can work on your machine.`); - } - } - } - - private async buildForDevice(projectRoot: string, args: string[], buildConfig: IBuildConfig, projectData: IProjectData): Promise { - if (!buildConfig.release && !buildConfig.architectures) { - await this.$devicesService.initialize({ - platform: this.$devicePlatformsConstants.iOS.toLowerCase(), deviceId: buildConfig.device, - skipEmulatorStart: true - }); - const instances = this.$devicesService.getDeviceInstances(); - const devicesArchitectures = _(instances) - .filter(d => this.$mobileHelper.isiOSPlatform(d.deviceInfo.platform) && !!d.deviceInfo.activeArchitecture) - .map(d => d.deviceInfo.activeArchitecture) - .uniq() - .value(); - if (devicesArchitectures.length > 0) { - const architectures = this.getBuildArchitectures(projectData, buildConfig, devicesArchitectures); - if (devicesArchitectures.length > 1) { - architectures.push('ONLY_ACTIVE_ARCH=NO'); - } - buildConfig.architectures = architectures; - } - } - - args = args.concat((buildConfig && buildConfig.architectures) || this.getBuildArchitectures(projectData, buildConfig, ["armv7", "arm64"])); - - if (!this.hasWatchApp(projectData)) { - args = args.concat([ - "-sdk", DevicePlatformSdkName - ]); - } - - args = args.concat([ - "BUILD_DIR=" + path.join(projectRoot, constants.BUILD_DIR) - ]); - - const xcodeBuildVersion = await this.getXcodeVersion(); - if (helpers.versionCompare(xcodeBuildVersion, "8.0") >= 0) { - await this.setupSigningForDevice(projectRoot, buildConfig, projectData); - } - - await this.createIpa(projectRoot, projectData, buildConfig, args); - } - - private async xcodebuild(args: string[], cwd: string, stdio: any = "inherit"): Promise { - const localArgs = [...args]; - const xcodeBuildVersion = await this.getXcodeVersion(); - try { - if (helpers.versionCompare(xcodeBuildVersion, "9.0") >= 0) { - localArgs.push("-allowProvisioningUpdates"); - } - } catch (e) { - this.$logger.warn("Failed to detect whether -allowProvisioningUpdates can be used with your xcodebuild version due to error: " + e); - } - if (this.$logger.getLevel() === "INFO") { - localArgs.push("-quiet"); - this.$logger.info("Xcode build..."); - } - - let commandResult; - try { - commandResult = await this.$childProcess.spawnFromEvent("xcodebuild", - localArgs, - "exit", - { stdio: stdio || "inherit", cwd }, - { emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true }); - } catch (err) { - this.$errors.failWithoutHelp(err.message); - } - - return commandResult; - } - - private getBuildArchitectures(projectData: IProjectData, buildConfig: IBuildConfig, architectures: string[]): string[] { - let result: string[] = []; - - const frameworkVersion = this.getFrameworkVersion(projectData); - if (semver.valid(frameworkVersion) && semver.lt(semver.coerce(frameworkVersion), "5.1.0")) { - const target = this.getDeploymentTarget(projectData); - if (target && target.major >= 11) { - // We need to strip 32bit architectures as of deployment target >= 11 it is not allowed to have such - architectures = _.filter(architectures, arch => { - const is64BitArchitecture = arch === "x86_64" || arch === "arm64"; - if (!is64BitArchitecture) { - this.$logger.warn(`The architecture ${arch} will be stripped as it is not supported for deployment target ${target.version}.`); - } - return is64BitArchitecture; - }); - } - result = [`ARCHS=${architectures.join(" ")}`, `VALID_ARCHS=${architectures.join(" ")}`]; - } - - return result; - } - - private async setupSigningFromTeam(projectRoot: string, projectData: IProjectData, teamId: string) { - const xcode = this.$pbxprojDomXcode.Xcode.open(this.getPbxProjPath(projectData)); - const signing = xcode.getSigning(projectData.projectName); - - let shouldUpdateXcode = false; - if (signing && signing.style === "Automatic") { - if (signing.team !== teamId) { - // Maybe the provided team is name such as "Telerik AD" and we need to convert it to CH******37 - const teamIdsForName = await this.$iOSProvisionService.getTeamIdsWithName(teamId); - if (!teamIdsForName.some(id => id === signing.team)) { - shouldUpdateXcode = true; - } - } - } else { - shouldUpdateXcode = true; - } - - if (shouldUpdateXcode) { - const teamIdsForName = await this.$iOSProvisionService.getTeamIdsWithName(teamId); - if (teamIdsForName.length > 0) { - this.$logger.trace(`Team id ${teamIdsForName[0]} will be used for team name "${teamId}".`); - teamId = teamIdsForName[0]; - } - - xcode.setAutomaticSigningStyle(projectData.projectName, teamId); - xcode.setAutomaticSigningStyleByTargetProductTypesList([ - IOSNativeTargetProductTypes.appExtension, - IOSNativeTargetProductTypes.watchApp, - IOSNativeTargetProductTypes.watchExtension - ], - teamId); - xcode.save(); - - this.$logger.trace(`Set Automatic signing style and team id ${teamId}.`); - } else { - this.$logger.trace(`The specified ${teamId} is already set in the Xcode.`); - } - } - - private async setupSigningFromProvision(projectRoot: string, projectData: IProjectData, provision?: string, mobileProvisionData?: mobileProvisionFinder.provision.MobileProvision): Promise { - if (provision) { - const xcode = this.$pbxprojDomXcode.Xcode.open(this.getPbxProjPath(projectData)); - const signing = xcode.getSigning(projectData.projectName); - - let shouldUpdateXcode = false; - if (signing && signing.style === "Manual") { - for (const config in signing.configurations) { - const options = signing.configurations[config]; - if (options.name !== provision && options.uuid !== provision) { - shouldUpdateXcode = true; - break; - } - } - } else { - shouldUpdateXcode = true; - } - - if (shouldUpdateXcode) { - const pickStart = Date.now(); - const mobileprovision = mobileProvisionData || await this.$iOSProvisionService.pick(provision, projectData.projectIdentifiers.ios); - const pickEnd = Date.now(); - this.$logger.trace("Searched and " + (mobileprovision ? "found" : "failed to find ") + " matching provisioning profile. (" + (pickEnd - pickStart) + "ms.)"); - if (!mobileprovision) { - this.$errors.failWithoutHelp("Failed to find mobile provision with UUID or Name: " + provision); - } - const configuration = { - team: mobileprovision.TeamIdentifier && mobileprovision.TeamIdentifier.length > 0 ? mobileprovision.TeamIdentifier[0] : undefined, - uuid: mobileprovision.UUID, - name: mobileprovision.Name, - identity: mobileprovision.Type === "Development" ? "iPhone Developer" : "iPhone Distribution" - }; - xcode.setManualSigningStyle(projectData.projectName, configuration); - xcode.setManualSigningStyleByTargetProductTypesList([ - IOSNativeTargetProductTypes.appExtension, - IOSNativeTargetProductTypes.watchApp, - IOSNativeTargetProductTypes.watchExtension - ], - configuration); - xcode.save(); - - // this.cache(uuid); - this.$logger.trace(`Set Manual signing style and provisioning profile: ${mobileprovision.Name} (${mobileprovision.UUID})`); - } else { - this.$logger.trace(`The specified provisioning profile is already set in the Xcode: ${provision}`); - } - } else { - // read uuid from Xcode and cache... - } - } - - private async setupSigningForDevice(projectRoot: string, buildConfig: IiOSBuildConfig, projectData: IProjectData): Promise { - const xcode = this.$pbxprojDomXcode.Xcode.open(this.getPbxProjPath(projectData)); - const signing = xcode.getSigning(projectData.projectName); - - const hasProvisioningProfileInXCConfig = - this.readXCConfigProvisioningProfileSpecifierForIPhoneOs(projectData) || - this.readXCConfigProvisioningProfileSpecifier(projectData) || - this.readXCConfigProvisioningProfileForIPhoneOs(projectData) || - this.readXCConfigProvisioningProfile(projectData); - - 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, buildConfig.teamId); - await this.setupSigningFromTeam(projectRoot, projectData, teamId); - } - } - - private async buildForSimulator(projectRoot: string, args: string[], projectData: IProjectData, buildConfig?: IBuildConfig): Promise { - const architectures = this.getBuildArchitectures(projectData, buildConfig, ["i386", "x86_64"]); - let product; - - args = args - .concat(architectures) - .concat([ - "build", - "-configuration", getConfigurationName(buildConfig.release), - "ONLY_ACTIVE_ARCH=NO", - "BUILD_DIR=" + path.join(projectRoot, constants.BUILD_DIR), - ]); - - if (this.hasWatchApp(projectData)) { - product = ProductArgs.scheme; - args = args.concat(["-destination", "generic/platform=iOS Simulator", "CODE_SIGNING_ALLOWED=NO"]); - } else { - args = args.concat(["-sdk", SimulatorPlatformSdkName, "CODE_SIGN_IDENTITY="]); - } - - args = args.concat(this.xcbuildProjectArgs(projectRoot, projectData, product)); - - await this.xcodebuild(args, projectRoot, buildConfig.buildOutputStdio); - } - - private async createIpa(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig, args: string[]): Promise { - const archivePath = await this.archive(projectData, buildConfig, { additionalArgs: args }); - const exportFileIpa = await this.exportDevelopmentArchive(projectData, buildConfig, { archivePath, provision: buildConfig.provision || buildConfig.mobileProvisionIdentifier }); - return exportFileIpa; - } - public isPlatformPrepared(projectRoot: string, projectData: IProjectData): boolean { return this.$fs.exists(path.join(projectRoot, projectData.projectName, constants.APP_FOLDER_NAME)); } @@ -690,105 +279,22 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ return contentIsTheSame; } - /** - * Patch **LaunchScreen.xib** so we can be backward compatible for eternity. - * The **xcodeproj** template proior v**2.1.0** had blank white screen launch screen. - * We extended that by adding **app/AppResources/iOS/LaunchScreen.storyboard** - * However for projects created prior **2.1.0** to keep working without the obsolete **LaunchScreen.xib** - * we must still provide it on prepare. - * Here we check if **UILaunchStoryboardName** is set to **LaunchScreen** in the **platform/ios//-Info.plist**. - * If it is, and no **LaunchScreen.storyboard** nor **.xib** is found in the project, we will create one. - */ - private provideLaunchScreenIfMissing(projectData: IProjectData): void { - try { - this.$logger.trace("Checking if we need to provide compatability LaunchScreen.xib"); - const platformData = this.getPlatformData(projectData); - const projectPath = path.join(platformData.projectRoot, projectData.projectName); - const projectPlist = this.getInfoPlistPath(projectData); - const plistContent = plist.parse(this.$fs.readText(projectPlist)); - const storyName = plistContent["UILaunchStoryboardName"]; - this.$logger.trace(`Examining ${projectPlist} UILaunchStoryboardName: "${storyName}".`); - if (storyName !== "LaunchScreen") { - this.$logger.trace("The project has its UILaunchStoryboardName set to " + storyName + " which is not the pre v2.1.0 default LaunchScreen, probably the project is migrated so we are good to go."); - return; - } - - const expectedStoryPath = path.join(projectPath, "Resources", "LaunchScreen.storyboard"); - if (this.$fs.exists(expectedStoryPath)) { - // Found a LaunchScreen on expected path - this.$logger.trace("LaunchScreen.storyboard was found. Project is up to date."); - return; - } - this.$logger.trace("LaunchScreen file not found at: " + expectedStoryPath); - - const expectedXibPath = path.join(projectPath, "en.lproj", "LaunchScreen.xib"); - if (this.$fs.exists(expectedXibPath)) { - this.$logger.trace("Obsolete LaunchScreen.xib was found. It'k OK, we are probably running with iOS runtime from pre v2.1.0."); - return; - } - this.$logger.trace("LaunchScreen file not found at: " + expectedXibPath); - - const isTheLaunchScreenFile = (fileName: string) => fileName === "LaunchScreen.xib" || fileName === "LaunchScreen.storyboard"; - const matches = this.$fs.enumerateFilesInDirectorySync(projectPath, isTheLaunchScreenFile, { enumerateDirectories: false }); - if (matches.length > 0) { - this.$logger.trace("Found LaunchScreen by slowly traversing all files here: " + matches + "\nConsider moving the LaunchScreen so it could be found at: " + expectedStoryPath); - return; - } - - const compatabilityXibPath = path.join(projectPath, "Resources", "LaunchScreen.xib"); - this.$logger.warn(`Failed to find LaunchScreen.storyboard but it was specified in the Info.plist. -Consider updating the resources in app/App_Resources/iOS/. -A good starting point would be to create a new project and diff the changes with your current one. -Also the following repo may be helpful: https://github.com/NativeScript/template-hello-world/tree/master/App_Resources/iOS -We will now place an empty obsolete compatability white screen LauncScreen.xib for you in ${path.relative(projectData.projectDir, compatabilityXibPath)} so your app may appear as it did in pre v2.1.0 versions of the ios runtime.`); - - const content = ` - - - - - - - - - - - - - - - - -`; - try { - this.$fs.createDirectory(path.dirname(compatabilityXibPath)); - this.$fs.writeFile(compatabilityXibPath, content); - } catch (e) { - this.$logger.warn("We have failed to add compatability LaunchScreen.xib due to: " + e); - } - } catch (e) { - this.$logger.warn("We have failed to check if we need to add a compatability LaunchScreen.xib due to: " + e); - } - } - - public async prepareProject(projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise { + public async prepareProject(projectData: IProjectData, prepareData: IOSPrepareData): Promise { const projectRoot = path.join(projectData.platformsDir, "ios"); const platformData = this.getPlatformData(projectData); const resourcesDirectoryPath = projectData.getAppResourcesDirectoryPath(); - const provision = platformSpecificData && platformSpecificData.provision; - const teamId = platformSpecificData && platformSpecificData.teamId; + const provision = prepareData && prepareData.provision; + const teamId = prepareData && prepareData.teamId; if (provision) { - await this.setupSigningFromProvision(projectRoot, projectData, provision, platformSpecificData.mobileProvisionData); + await this.$iOSSigningService.setupSigningFromProvision(projectRoot, projectData, provision, prepareData.mobileProvisionData); } if (teamId) { - await this.setupSigningFromTeam(projectRoot, projectData, teamId); + await this.$iOSSigningService.setupSigningFromTeam(projectRoot, projectData, teamId); } const project = this.createPbxProj(projectData); - this.provideLaunchScreenIfMissing(projectData); - const resources = project.pbxGroupByName("Resources"); if (resources) { @@ -827,66 +333,42 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f if (addedWatchApp) { this.$logger.warn("The support for Apple Watch App is currently in Beta. For more information about the current development state and any known issues, please check the relevant GitHub issue: https://github.com/NativeScript/nativescript-cli/issues/4589"); } - } - public prepareAppResources(appResourcesDirectoryPath: string, projectData: IProjectData): void { - const platformFolder = path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName); - const filterFile = (filename: string) => this.$fs.deleteFile(path.join(platformFolder, filename)); + public prepareAppResources(projectData: IProjectData): void { + const platformData = this.getPlatformData(projectData); + const projectAppResourcesPath = projectData.getAppResourcesDirectoryPath(projectData.projectDir); + const platformsAppResourcesPath = this.getAppResourcesDestinationDirectoryPath(projectData); + + this.$fs.deleteDirectory(platformsAppResourcesPath); + this.$fs.ensureDirectoryExists(platformsAppResourcesPath); - filterFile(this.getPlatformData(projectData).configurationFileName); - filterFile(constants.PODFILE_NAME); + this.$fs.copyFile(path.join(projectAppResourcesPath, platformData.normalizedPlatformName, "*"), platformsAppResourcesPath); - // src folder should not be copied as the pbxproject will have references to its files - this.$fs.deleteDirectory(path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, constants.NATIVE_SOURCE_FOLDER)); - this.$fs.deleteDirectory(path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, constants.NATIVE_EXTENSION_FOLDER)); - this.$fs.deleteDirectory(path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, "watchapp")); - this.$fs.deleteDirectory(path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, "watchextension")); + this.$fs.deleteFile(path.join(platformsAppResourcesPath, platformData.configurationFileName)); + this.$fs.deleteFile(path.join(platformsAppResourcesPath, constants.PODFILE_NAME)); - this.$fs.deleteDirectory(this.getAppResourcesDestinationDirectoryPath(projectData)); + this.$fs.deleteDirectory(path.join(platformsAppResourcesPath, constants.NATIVE_SOURCE_FOLDER)); + this.$fs.deleteDirectory(path.join(platformsAppResourcesPath, constants.NATIVE_EXTENSION_FOLDER)); + this.$fs.deleteDirectory(path.join(platformsAppResourcesPath, "watchapp")); + this.$fs.deleteDirectory(path.join(platformsAppResourcesPath, "watchextension")); } public async processConfigurationFilesFromAppResources(projectData: IProjectData, opts: IRelease): Promise { await this.mergeInfoPlists(projectData, opts); await this.$iOSEntitlementsService.merge(projectData); await this.mergeProjectXcconfigFiles(projectData); - for (const pluginData of await this.getAllInstalledPlugins(projectData)) { - await this.$pluginVariablesService.interpolatePluginVariables(pluginData, this.getPlatformData(projectData).configurationFilePath, projectData.projectDir); - } - - this.$pluginVariablesService.interpolateAppIdentifier(this.getPlatformData(projectData).configurationFilePath, projectData.projectIdentifiers.ios); - } - - private getInfoPlistPath(projectData: IProjectData): string { - return path.join( - projectData.appResourcesDirectoryPath, - this.getPlatformData(projectData).normalizedPlatformName, - this.getPlatformData(projectData).configurationFileName - ); } public ensureConfigurationFileInAppResources(): void { return null; } - public async stopServices(): Promise { - return { stderr: "", stdout: "", exitCode: 0 }; - } - - public async cleanProject(projectRoot: string): Promise { - return Promise.resolve(); - } - private async mergeInfoPlists(projectData: IProjectData, buildOptions: IRelease): Promise { const projectDir = projectData.projectDir; const infoPlistPath = path.join(projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, this.getPlatformData(projectData).configurationFileName); this.ensureConfigurationFileInAppResources(); - if (!this.$fs.exists(infoPlistPath)) { - this.$logger.trace("Info.plist: No app/App_Resources/iOS/Info.plist found, falling back to pre-1.6.0 Info.plist behavior."); - return; - } - const reporterTraceMessage = "Info.plist:"; const reporter: Reporter = { log: (txt: string) => this.$logger.trace(`${reporterTraceMessage} ${txt}`), @@ -978,7 +460,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } private getPbxProjPath(projectData: IProjectData): string { - return path.join(this.$xcprojService.getXcodeprojPath(projectData, this.getPlatformData(projectData)), "project.pbxproj"); + return path.join(this.$xcprojService.getXcodeprojPath(projectData, this.getPlatformData(projectData).projectRoot), "project.pbxproj"); } private createPbxProj(projectData: IProjectData): any { @@ -1025,7 +507,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f const projectPodfilePath = this.$cocoapodsService.getProjectPodfilePath(platformData.projectRoot); if (this.$fs.exists(projectPodfilePath)) { - await this.$cocoapodsService.executePodInstall(platformData.projectRoot, this.$xcprojService.getXcodeprojPath(projectData, platformData)); + await this.$cocoapodsService.executePodInstall(platformData.projectRoot, this.$xcprojService.getXcodeprojPath(projectData, platformData.projectRoot)); // The `pod install` command adds a new target to the .pbxproject. This target adds additional build phases to Xcode project. // Some of these phases relies on env variables (like PODS_PODFILE_DIR_PATH or PODS_ROOT). // These variables are produced from merge of pod's xcconfig file and project's xcconfig file. @@ -1037,11 +519,13 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f this.$iOSExtensionsService.removeExtensions({ pbxProjPath }); await this.addExtensions(projectData); } + public beforePrepareAllPlugins(): Promise { 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) { @@ -1088,19 +572,9 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } } - public async prebuildNativePlugin(options: IPluginBuildOptions): Promise { /** */ } - - public async checkIfPluginsNeedBuild(projectData: IProjectData): Promise> { - return []; - } - - public getDeploymentTarget(projectData: IProjectData): semver.SemVer { + public getDeploymentTarget(projectData: IProjectData): string { const target = this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "IPHONEOS_DEPLOYMENT_TARGET"); - if (!target) { - return null; - } - - return semver.coerce(target); + return target; } private getAllLibsForPluginWithFileExtension(pluginData: IPluginData, fileExtension: string): string[] { @@ -1317,101 +791,12 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } } - private async getXcodeVersion(): Promise { - let xcodeBuildVersion = ""; - - try { - xcodeBuildVersion = await this.$sysInfo.getXcodeVersion(); - } catch (error) { - this.$errors.fail("xcodebuild execution failed. Make sure that you have latest Xcode and tools installed."); - } - - const splitedXcodeBuildVersion = xcodeBuildVersion.split("."); - xcodeBuildVersion = `${splitedXcodeBuildVersion[0] || 0}.${splitedXcodeBuildVersion[1] || 0}`; - - return xcodeBuildVersion; - } - private getBuildXCConfigFilePath(projectData: IProjectData): string { const buildXCConfig = path.join(projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, BUILD_XCCONFIG_FILE_NAME); return buildXCConfig; } - private readTeamId(projectData: IProjectData): string { - let teamId = this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "DEVELOPMENT_TEAM"); - - const fileName = path.join(this.getPlatformData(projectData).projectRoot, "teamid"); - if (this.$fs.exists(fileName)) { - teamId = this.$fs.readText(fileName); - } - - return teamId; - } - - private readXCConfigProvisioningProfile(projectData: IProjectData): string { - return this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE"); - } - - private readXCConfigProvisioningProfileForIPhoneOs(projectData: IProjectData): string { - return this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE[sdk=iphoneos*]"); - } - - private readXCConfigProvisioningProfileSpecifier(projectData: IProjectData): string { - return this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE_SPECIFIER"); - } - - private readXCConfigProvisioningProfileSpecifierForIPhoneOs(projectData: IProjectData): string { - return this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]"); - } - - private async getDevelopmentTeam(projectData: IProjectData, teamId?: string): Promise { - teamId = teamId || this.readTeamId(projectData); - - if (!teamId) { - const teams = await this.$iOSProvisionService.getDevelopmentTeams(); - this.$logger.warn("Xcode requires a team id to be specified when building for device."); - this.$logger.warn("You can specify the team id by setting the DEVELOPMENT_TEAM setting in build.xcconfig file located in App_Resources folder of your app, or by using the --teamId option when calling run, debug or livesync commands."); - if (teams.length === 1) { - teamId = teams[0].id; - this.$logger.warn("Found and using the following development team installed on your system: " + teams[0].name + " (" + teams[0].id + ")"); - } else if (teams.length > 0) { - if (!helpers.isInteractive()) { - this.$errors.failWithoutHelp(`Unable to determine default development team. Available development teams are: ${_.map(teams, team => team.id)}. Specify team in app/App_Resources/iOS/build.xcconfig file in the following way: DEVELOPMENT_TEAM = `); - } - - const choices: string[] = []; - for (const team of teams) { - choices.push(team.name + " (" + team.id + ")"); - } - const choice = await this.$prompter.promptForChoice('Found multiple development teams, select one:', choices); - teamId = teams[choices.indexOf(choice)].id; - - const choicesPersist = [ - "Yes, set the DEVELOPMENT_TEAM setting in build.xcconfig file.", - "Yes, persist the team id in platforms folder.", - "No, don't persist this setting." - ]; - const choicePersist = await this.$prompter.promptForChoice("Do you want to make teamId: " + teamId + " a persistent choice for your app?", choicesPersist); - switch (choicesPersist.indexOf(choicePersist)) { - case 0: - const xcconfigFile = path.join(projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, BUILD_XCCONFIG_FILE_NAME); - this.$fs.appendFile(xcconfigFile, "\nDEVELOPMENT_TEAM = " + teamId + "\n"); - break; - case 1: - this.$fs.writeFile(path.join(this.getPlatformData(projectData).projectRoot, "teamid"), teamId); - break; - default: - break; - } - } - } - - this.$logger.trace(`Selected teamId is '${teamId}'.`); - - return teamId; - } - private validateApplicationIdentifier(projectData: IProjectData): void { const infoPlistPath = path.join(projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, this.getPlatformData(projectData).configurationFileName); const mergedPlistPath = this.getPlatformData(projectData).configurationFilePath; @@ -1427,29 +812,6 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f this.$logger.warn("[WARNING]: The CFBundleIdentifier key inside the 'Info.plist' will be overriden by the 'id' inside 'package.json'."); } } - - private getExportOptionsMethod(projectData: IProjectData, archivePath: string): string { - const embeddedMobileProvisionPath = path.join(archivePath, 'Products', 'Applications', `${projectData.projectName}.app`, "embedded.mobileprovision"); - const provision = mobileProvisionFinder.provision.readFromFile(embeddedMobileProvisionPath); - - return { - "Development": "development", - "AdHoc": "ad-hoc", - "Distribution": "app-store", - "Enterprise": "enterprise" - }[provision.Type]; - } - - private hasWatchApp(projectData: IProjectData) { - const platformData = this.getPlatformData(projectData); - const watchAppPath = path.join( - projectData.getAppResourcesDirectoryPath(), - platformData.normalizedPlatformName, - constants.IOS_WATCHAPP_FOLDER - ); - - return this.$fs.exists(watchAppPath); - } } $injector.register("iOSProjectService", IOSProjectService); diff --git a/lib/services/ios-watch-app-service.ts b/lib/services/ios-watch-app-service.ts index dae287e9fb..d5b6886e45 100644 --- a/lib/services/ios-watch-app-service.ts +++ b/lib/services/ios-watch-app-service.ts @@ -61,6 +61,16 @@ export class IOSWatchAppService extends NativeTargetServiceBase implements IIOSW this.$fs.writeFile(pbxProjPath, project.writeSync({omitEmptyValues: true})); } + public hasWatchApp(platformData: IPlatformData, projectData: IProjectData): boolean { + const watchAppPath = path.join( + projectData.getAppResourcesDirectoryPath(), + platformData.normalizedPlatformName, + IOS_WATCHAPP_FOLDER + ); + + return this.$fs.exists(watchAppPath); + } + private configureTarget(targetName: string, targetPath: string, identifier: string, configurationFileName: string, target: IXcode.target, project: IXcode.project) { const targetConfigurationJsonPath = path.join(targetPath, configurationFileName); diff --git a/lib/services/ios/export-options-plist-service.ts b/lib/services/ios/export-options-plist-service.ts new file mode 100644 index 0000000000..d64a3dfe44 --- /dev/null +++ b/lib/services/ios/export-options-plist-service.ts @@ -0,0 +1,98 @@ +import * as path from "path"; +import * as temp from "temp"; +import * as mobileProvisionFinder from "ios-mobileprovision-finder"; + +export class ExportOptionsPlistService implements IExportOptionsPlistService { + constructor(private $fs: IFileSystem) { } + + public createDevelopmentExportOptionsPlist(archivePath: string, projectData: IProjectData, buildConfig: IBuildConfig): IExportOptionsPlistOutput { + const exportOptionsMethod = this.getExportOptionsMethod(projectData, archivePath); + const provision = buildConfig.provision || buildConfig.mobileProvisionIdentifier; + let plistTemplate = ` + + + + method + ${exportOptionsMethod}`; + if (provision) { + plistTemplate += ` provisioningProfiles + + ${projectData.projectIdentifiers.ios} + ${provision} +`; + } + plistTemplate += ` + uploadBitcode + + compileBitcode + + +`; + + // Save the options... + temp.track(); + const exportOptionsPlistFilePath = temp.path({ prefix: "export-", suffix: ".plist" }); + this.$fs.writeFile(exportOptionsPlistFilePath, plistTemplate); + + // The xcodebuild exportPath expects directory and writes the .ipa at that directory. + const exportFileDir = path.resolve(path.dirname(archivePath)); + const exportFilePath = path.join(exportFileDir, projectData.projectName + ".ipa"); + + return { exportFileDir, exportFilePath, exportOptionsPlistFilePath }; + } + + public createDistributionExportOptionsPlist(archivePath: string, projectData: IProjectData, buildConfig: IBuildConfig): IExportOptionsPlistOutput { + const provision = buildConfig.provision || buildConfig.mobileProvisionIdentifier; + const teamId = buildConfig.teamId; + let plistTemplate = ` + + + +`; + if (teamId) { + plistTemplate += ` teamID + ${teamId} +`; + } + if (provision) { + plistTemplate += ` provisioningProfiles + + ${projectData.projectIdentifiers.ios} + ${provision} + `; + } + plistTemplate += ` method + app-store + uploadBitcode + + compileBitcode + + uploadSymbols + + +`; + + // Save the options... + temp.track(); + const exportOptionsPlistFilePath = temp.path({ prefix: "export-", suffix: ".plist" }); + this.$fs.writeFile(exportOptionsPlistFilePath, plistTemplate); + + const exportFileDir = path.resolve(path.dirname(archivePath)); + const exportFilePath = path.join(exportFileDir, projectData.projectName + ".ipa"); + + return { exportFileDir, exportFilePath, exportOptionsPlistFilePath }; + } + + private getExportOptionsMethod(projectData: IProjectData, archivePath: string): string { + const embeddedMobileProvisionPath = path.join(archivePath, 'Products', 'Applications', `${projectData.projectName}.app`, "embedded.mobileprovision"); + const provision = mobileProvisionFinder.provision.readFromFile(embeddedMobileProvisionPath); + + return { + "Development": "development", + "AdHoc": "ad-hoc", + "Distribution": "app-store", + "Enterprise": "enterprise" + }[provision.Type]; + } +} +$injector.register("exportOptionsPlistService", ExportOptionsPlistService); diff --git a/lib/services/ios/ios-signing-service.ts b/lib/services/ios/ios-signing-service.ts new file mode 100644 index 0000000000..58ea06b2b0 --- /dev/null +++ b/lib/services/ios/ios-signing-service.ts @@ -0,0 +1,205 @@ +import * as path from "path"; +import * as mobileProvisionFinder from "ios-mobileprovision-finder"; +import { BUILD_XCCONFIG_FILE_NAME, iOSAppResourcesFolderName, IOSNativeTargetProductTypes } 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( + private $errors: IErrors, + private $fs: IFileSystem, + private $iOSProvisionService: IOSProvisionService, + private $logger: ILogger, + private $pbxprojDomXcode: IPbxprojDomXcode, + private $prompter: IPrompter, + private $xcconfigService: IXcconfigService, + private $xcprojService: IXcprojService + ) { } + + 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); + + const hasProvisioningProfileInXCConfig = + this.readXCConfigProvisioningProfileSpecifierForIPhoneOs(projectData) || + this.readXCConfigProvisioningProfileSpecifier(projectData) || + this.readXCConfigProvisioningProfileForIPhoneOs(projectData) || + this.readXCConfigProvisioningProfile(projectData); + + if (hasProvisioningProfileInXCConfig && (!signing || signing.style !== "Manual")) { + xcode.setManualSigningStyle(projectData.projectName); + xcode.save(); + } 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); + } + } + + public async setupSigningFromTeam(projectRoot: string, projectData: IProjectData, teamId: string): Promise { + const xcode = this.$pbxprojDomXcode.Xcode.open(this.getPbxProjPath(projectData, projectRoot)); + const signing = xcode.getSigning(projectData.projectName); + + let shouldUpdateXcode = false; + if (signing && signing.style === "Automatic") { + if (signing.team !== teamId) { + // Maybe the provided team is name such as "Telerik AD" and we need to convert it to CH******37 + const teamIdsForName = await this.$iOSProvisionService.getTeamIdsWithName(teamId); + if (!teamIdsForName.some(id => id === signing.team)) { + shouldUpdateXcode = true; + } + } + } else { + shouldUpdateXcode = true; + } + + if (shouldUpdateXcode) { + const teamIdsForName = await this.$iOSProvisionService.getTeamIdsWithName(teamId); + if (teamIdsForName.length > 0) { + this.$logger.trace(`Team id ${teamIdsForName[0]} will be used for team name "${teamId}".`); + teamId = teamIdsForName[0]; + } + + xcode.setAutomaticSigningStyle(projectData.projectName, teamId); + xcode.setAutomaticSigningStyleByTargetProductTypesList([ + IOSNativeTargetProductTypes.appExtension, + IOSNativeTargetProductTypes.watchApp, + IOSNativeTargetProductTypes.watchExtension + ], + teamId); + xcode.save(); + + this.$logger.trace(`Set Automatic signing style and team id ${teamId}.`); + } else { + this.$logger.trace(`The specified ${teamId} is already set in the Xcode.`); + } + } + + public async setupSigningFromProvision(projectRoot: string, projectData: IProjectData, provision?: string, mobileProvisionData?: mobileProvisionFinder.provision.MobileProvision): Promise { + if (!provision) { + // read uuid from Xcode an cache... + return; + } + + const xcode = this.$pbxprojDomXcode.Xcode.open(this.getPbxProjPath(projectData, projectRoot)); + const signing = xcode.getSigning(projectData.projectName); + + let shouldUpdateXcode = false; + if (signing && signing.style === "Manual") { + for (const config in signing.configurations) { + const options = signing.configurations[config]; + if (options.name !== provision && options.uuid !== provision) { + shouldUpdateXcode = true; + break; + } + } + } else { + shouldUpdateXcode = true; + } + + if (shouldUpdateXcode) { + const pickStart = Date.now(); + const mobileprovision = mobileProvisionData || await this.$iOSProvisionService.pick(provision, projectData.projectIdentifiers.ios); + const pickEnd = Date.now(); + this.$logger.trace("Searched and " + (mobileprovision ? "found" : "failed to find ") + " matching provisioning profile. (" + (pickEnd - pickStart) + "ms.)"); + if (!mobileprovision) { + this.$errors.failWithoutHelp("Failed to find mobile provision with UUID or Name: " + provision); + } + const configuration = { + team: mobileprovision.TeamIdentifier && mobileprovision.TeamIdentifier.length > 0 ? mobileprovision.TeamIdentifier[0] : undefined, + uuid: mobileprovision.UUID, + name: mobileprovision.Name, + identity: mobileprovision.Type === "Development" ? "iPhone Developer" : "iPhone Distribution" + }; + xcode.setManualSigningStyle(projectData.projectName, configuration); + xcode.setManualSigningStyleByTargetProductTypesList([ + IOSNativeTargetProductTypes.appExtension, + IOSNativeTargetProductTypes.watchApp, + IOSNativeTargetProductTypes.watchExtension + ], + configuration); + xcode.save(); + + // this.cache(uuid); + this.$logger.trace(`Set Manual signing style and provisioning profile: ${mobileprovision.Name} (${mobileprovision.UUID})`); + } else { + this.$logger.trace(`The specified provisioning profile is already set in the Xcode: ${provision}`); + } + } + + private getBuildXCConfigFilePath(projectData: IProjectData): string { + return path.join(projectData.appResourcesDirectoryPath, iOSAppResourcesFolderName, BUILD_XCCONFIG_FILE_NAME); + } + + private getPbxProjPath(projectData: IProjectData, projectRoot: string): string { + return path.join(this.$xcprojService.getXcodeprojPath(projectData, projectRoot), "project.pbxproj"); + } + + private async getDevelopmentTeam(projectData: IProjectData, projectRoot: string, teamId?: string): Promise { + teamId = teamId || this.readXCConfigDevelopmentTeam(projectData); + + if (!teamId) { + const teams = await this.$iOSProvisionService.getDevelopmentTeams(); + this.$logger.warn("Xcode requires a team id to be specified when building for device."); + this.$logger.warn("You can specify the team id by setting the DEVELOPMENT_TEAM setting in build.xcconfig file located in App_Resources folder of your app, or by using the --teamId option when calling run, debug or livesync commands."); + if (teams.length === 1) { + teamId = teams[0].id; + this.$logger.warn("Found and using the following development team installed on your system: " + teams[0].name + " (" + teams[0].id + ")"); + } else if (teams.length > 0) { + if (!helpers.isInteractive()) { + this.$errors.failWithoutHelp(`Unable to determine default development team. Available development teams are: ${_.map(teams, team => team.id)}. Specify team in app/App_Resources/iOS/build.xcconfig file in the following way: DEVELOPMENT_TEAM = `); + } + + const choices: string[] = []; + for (const team of teams) { + choices.push(team.name + " (" + team.id + ")"); + } + const choice = await this.$prompter.promptForChoice('Found multiple development teams, select one:', choices); + teamId = teams[choices.indexOf(choice)].id; + + const choicesPersist = [ + "Yes, set the DEVELOPMENT_TEAM setting in build.xcconfig file.", + "Yes, persist the team id in platforms folder.", + "No, don't persist this setting." + ]; + const choicePersist = await this.$prompter.promptForChoice("Do you want to make teamId: " + teamId + " a persistent choice for your app?", choicesPersist); + switch (choicesPersist.indexOf(choicePersist)) { + case 0: + const xcconfigFile = path.join(projectData.appResourcesDirectoryPath, "iOS", BUILD_XCCONFIG_FILE_NAME); + this.$fs.appendFile(xcconfigFile, "\nDEVELOPMENT_TEAM = " + teamId + "\n"); + break; + case 1: + this.$fs.writeFile(path.join(projectRoot, "teamid"), teamId); + break; + default: + break; + } + } + } + + this.$logger.trace(`Selected teamId is '${teamId}'.`); + + return teamId; + } + + private readXCConfigDevelopmentTeam(projectData: IProjectData): string { + return this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "DEVELOPMENT_TEAM"); + } + + private readXCConfigProvisioningProfile(projectData: IProjectData): string { + return this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE"); + } + + private readXCConfigProvisioningProfileForIPhoneOs(projectData: IProjectData): string { + return this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE[sdk=iphoneos*]"); + } + + private readXCConfigProvisioningProfileSpecifier(projectData: IProjectData): string { + return this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE_SPECIFIER"); + } + + private readXCConfigProvisioningProfileSpecifierForIPhoneOs(projectData: IProjectData): string { + return this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]"); + } +} +$injector.register("iOSSigningService", IOSSigningService); diff --git a/lib/services/ios/xcodebuild-args-service.ts b/lib/services/ios/xcodebuild-args-service.ts new file mode 100644 index 0000000000..8471d58873 --- /dev/null +++ b/lib/services/ios/xcodebuild-args-service.ts @@ -0,0 +1,119 @@ +import * as path from "path"; +import * as constants from "../../constants"; +import { Configurations } from "../../common/constants"; + +const DevicePlatformSdkName = "iphoneos"; +const SimulatorPlatformSdkName = "iphonesimulator"; +enum ProductArgs { + target = "target", + scheme = "scheme" +} + +export class XcodebuildArgsService implements IXcodebuildArgsService { + + constructor( + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $devicesService: Mobile.IDevicesService, + private $fs: IFileSystem, + private $iOSWatchAppService: IIOSWatchAppService, + private $logger: ILogger + ) { } + + public async getBuildForSimulatorArgs(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + let args = await this.getArchitecturesArgs(buildConfig); + + let productType: ProductArgs; + if (this.$iOSWatchAppService.hasWatchApp(platformData, projectData)) { + productType = ProductArgs.scheme; + args = args.concat(["-destination", "generic/platform=iOS Simulator", "CODE_SIGNING_ALLOWED=NO"]); + } else { + args = args.concat(["CODE_SIGN_IDENTITY="]); + } + + args = args + .concat([ + "build", + "-configuration", buildConfig.release ? Configurations.Release : Configurations.Debug + ]) + .concat(this.getBuildCommonArgs(platformData, projectData, SimulatorPlatformSdkName)) + .concat(this.getBuildLoggingArgs()) + .concat(this.getXcodeProjectArgs(platformData.projectRoot, projectData, productType)); + + return args; + } + + public async getBuildForDeviceArgs(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + const architectures = await this.getArchitecturesArgs(buildConfig); + const archivePath = path.join(platformData.getBuildOutputPath(buildConfig), projectData.projectName + ".xcarchive"); + const args = [ + "archive", + "-archivePath", archivePath, + "-configuration", buildConfig.release ? Configurations.Release : Configurations.Debug + ] + .concat(this.getXcodeProjectArgs(platformData.projectRoot, projectData, ProductArgs.scheme)) + .concat(architectures) + .concat(this.getBuildCommonArgs(platformData, projectData, DevicePlatformSdkName)) + .concat(this.getBuildLoggingArgs()); + + return args; + } + + private async getArchitecturesArgs(buildConfig: IBuildConfig): Promise { + const args = []; + + const devicesArchitectures = buildConfig.buildForDevice ? await this.getArchitecturesFromConnectedDevices(buildConfig) : []; + if (!buildConfig.buildForDevice || devicesArchitectures.length > 1) { + args.push("ONLY_ACTIVE_ARCH=NO"); + } + + return args; + } + + private getXcodeProjectArgs(projectRoot: string, projectData: IProjectData, product?: ProductArgs): string[] { + const xcworkspacePath = path.join(projectRoot, `${projectData.projectName}.xcworkspace`); + if (this.$fs.exists(xcworkspacePath)) { + return [ "-workspace", xcworkspacePath, "-scheme", projectData.projectName ]; + } + + const xcodeprojPath = path.join(projectRoot, `${projectData.projectName}.xcodeproj`); + return [ "-project", xcodeprojPath, product ? "-" + product : "-target", projectData.projectName ]; + } + + private getBuildLoggingArgs(): string[] { + return this.$logger.getLevel() === "INFO" ? ["-quiet"] : []; + } + + private getBuildCommonArgs(platformData: IPlatformData, projectData: IProjectData, platformSdkName: string): string[] { + let args: string[] = []; + + if (!this.$iOSWatchAppService.hasWatchApp(platformData, projectData)) { + args = args.concat([ "-sdk", platformSdkName ]); + } + + args = args.concat([ + "BUILD_DIR=" + path.join(platformData.projectRoot, constants.BUILD_DIR), + 'SHARED_PRECOMPS_DIR=' + path.join(platformData.projectRoot, 'build', 'sharedpch'), + '-allowProvisioningUpdates' + ]); + + return args; + } + + private async getArchitecturesFromConnectedDevices(buildConfig: IiOSBuildConfig): Promise { + const platform = this.$devicePlatformsConstants.iOS.toLowerCase(); + await this.$devicesService.initialize({ + platform, + deviceId: buildConfig.device, + skipEmulatorStart: true + }); + const instances = this.$devicesService.getDevicesForPlatform(platform); + const architectures = _(instances) + .map(d => d.deviceInfo.activeArchitecture) + .filter(d => !!d) + .uniq() + .value(); + + return architectures; + } +} +$injector.register("xcodebuildArgsService", XcodebuildArgsService); diff --git a/lib/services/ios/xcodebuild-command-service.ts b/lib/services/ios/xcodebuild-command-service.ts new file mode 100644 index 0000000000..172a19b873 --- /dev/null +++ b/lib/services/ios/xcodebuild-command-service.ts @@ -0,0 +1,29 @@ +import * as constants from "../../constants"; + +export class XcodebuildCommandService implements IXcodebuildCommandService { + constructor( + private $childProcess: IChildProcess, + private $errors: IErrors, + private $logger: ILogger + ) { } + + public async executeCommand(args: string[], options: { cwd: string, stdio: string, message?: string, spawnOptions?: any }): Promise { + const { message, cwd, stdio, spawnOptions } = options; + this.$logger.info(message || "Xcode build..."); + + const childProcessOptions = { cwd, stdio: stdio || "inherit" }; + + try { + const commandResult = await this.$childProcess.spawnFromEvent("xcodebuild", + args, + "exit", + childProcessOptions, + spawnOptions || { emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true }); + + return commandResult; + } catch (err) { + this.$errors.failWithoutHelp(err.message); + } + } +} +$injector.register("xcodebuildCommandService", XcodebuildCommandService); diff --git a/lib/services/ios/xcodebuild-service.ts b/lib/services/ios/xcodebuild-service.ts new file mode 100644 index 0000000000..d1a3affef9 --- /dev/null +++ b/lib/services/ios/xcodebuild-service.ts @@ -0,0 +1,58 @@ +import * as path from "path"; + +export class XcodebuildService implements IXcodebuildService { + constructor( + private $exportOptionsPlistService: IExportOptionsPlistService, + private $xcodebuildArgsService: IXcodebuildArgsService, + private $xcodebuildCommandService: IXcodebuildCommandService + ) { } + + public async buildForDevice(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + const args = await this.$xcodebuildArgsService.getBuildForDeviceArgs(platformData, projectData, buildConfig); + await this.$xcodebuildCommandService.executeCommand(args, { cwd: platformData.projectRoot, stdio: buildConfig && buildConfig.buildOutputStdio }); + const archivePath = await this.createDevelopmentArchive(platformData, projectData, buildConfig); + return archivePath; + } + + public async buildForSimulator(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + const args = await this.$xcodebuildArgsService.getBuildForSimulatorArgs(platformData, projectData, buildConfig); + await this.$xcodebuildCommandService.executeCommand(args, { cwd: platformData.projectRoot, stdio: buildConfig.buildOutputStdio }); + } + + public async buildForAppStore(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + const args = await this.$xcodebuildArgsService.getBuildForDeviceArgs(platformData, projectData, buildConfig); + await this.$xcodebuildCommandService.executeCommand(args, { cwd: platformData.projectRoot, stdio: buildConfig && buildConfig.buildOutputStdio }); + const archivePath = await this.createDistributionArchive(platformData, projectData, buildConfig); + return archivePath; + } + + private async createDevelopmentArchive(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + const archivePath = path.join(platformData.getBuildOutputPath(buildConfig), projectData.projectName + ".xcarchive"); + const output = this.$exportOptionsPlistService.createDevelopmentExportOptionsPlist(archivePath, projectData, buildConfig); + const args = [ + "-exportArchive", + "-archivePath", archivePath, + "-exportPath", output.exportFileDir, + "-exportOptionsPlist", output.exportOptionsPlistFilePath + ]; + await this.$xcodebuildCommandService.executeCommand(args, { cwd: platformData.projectRoot, stdio: buildConfig.buildOutputStdio }); + + return output.exportFilePath; + } + + private async createDistributionArchive(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + const archivePath = path.join(platformData.getBuildOutputPath(buildConfig), projectData.projectName + ".xcarchive"); + const output = this.$exportOptionsPlistService.createDistributionExportOptionsPlist(archivePath, projectData, buildConfig); + const args = [ + "-exportArchive", + "-archivePath", archivePath, + "-exportPath", output.exportFileDir, + "-exportOptionsPlist", output.exportOptionsPlistFilePath + ]; + + await this.$xcodebuildCommandService.executeCommand(args, { cwd: platformData.projectRoot }); + + return output.exportFilePath; + } +} +$injector.register("xcodebuildService", XcodebuildService); diff --git a/lib/services/livesync-process-data-service.ts b/lib/services/livesync-process-data-service.ts new file mode 100644 index 0000000000..6edd76ef88 --- /dev/null +++ b/lib/services/livesync-process-data-service.ts @@ -0,0 +1,34 @@ +export class LiveSyncProcessDataService implements ILiveSyncProcessDataService { + protected processes: IDictionary = {}; + + public persistData(projectDir: string, deviceDescriptors: ILiveSyncDeviceDescriptor[], platforms: string[]): void { + this.processes[projectDir] = this.processes[projectDir] || Object.create(null); + this.processes[projectDir].actionsChain = this.processes[projectDir].actionsChain || Promise.resolve(); + this.processes[projectDir].currentSyncAction = this.processes[projectDir].actionsChain; + this.processes[projectDir].isStopped = false; + this.processes[projectDir].platforms = platforms; + + const currentDeviceDescriptors = this.getDeviceDescriptors(projectDir); + this.processes[projectDir].deviceDescriptors = _.uniqBy(currentDeviceDescriptors.concat(deviceDescriptors), "identifier"); + } + + public getPersistedData(projectDir: string): ILiveSyncProcessData { + return this.processes[projectDir]; + } + + public getDeviceDescriptors(projectDir: string): ILiveSyncDeviceDescriptor[] { + const liveSyncProcessesInfo = this.processes[projectDir] || {}; + const currentDescriptors = liveSyncProcessesInfo.deviceDescriptors; + return currentDescriptors || []; + } + + public hasDeviceDescriptors(projectDir: string): boolean { + const deviceDescriptors = this.getDeviceDescriptors(projectDir); + return !!deviceDescriptors.length; + } + + public getAllPersistedData() { + return this.processes; + } +} +$injector.register("liveSyncProcessDataService", LiveSyncProcessDataService); diff --git a/lib/services/livesync/android-device-livesync-service-base.ts b/lib/services/livesync/android-device-livesync-service-base.ts index d8aa61f351..1f3049232d 100644 --- a/lib/services/livesync/android-device-livesync-service-base.ts +++ b/lib/services/livesync/android-device-livesync-service-base.ts @@ -2,17 +2,17 @@ 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; public abstract async transferDirectoryOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string): Promise; - public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo, options: ITransferFilesOptions): Promise { + public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceDescriptor: ILiveSyncDeviceDescriptor, options: ITransferFilesOptions): Promise { const deviceHashService = this.device.fileSystem.getDeviceHashService(deviceAppData.appIdentifier); const currentHashes = await deviceHashService.generateHashesFromLocalToDevicePaths(localToDevicePaths); const transferredFiles = await this.transferFilesCore(deviceAppData, localToDevicePaths, projectFilesPath, currentHashes, options); 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..bd250bbbe4 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,13 +23,13 @@ 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; } @performanceLog() - public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo, options: ITransferFilesOptions): Promise { + public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceDescriptor: ILiveSyncDeviceDescriptor, options: ITransferFilesOptions): Promise { let transferredFiles: Mobile.ILocalToDevicePathData[] = []; if (options.isFullSync) { 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..70928ea873 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,8 +61,8 @@ 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, - liveSyncDeviceInfo: liveSyncInfo.liveSyncDeviceInfo, + device, + liveSyncDeviceData: liveSyncInfo.liveSyncDeviceData, 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 4bb3837a4a..0000000000 --- a/lib/services/livesync/livesync-service.ts +++ /dev/null @@ -1,882 +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, - platformTemplate: null, - 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..577a2f9048 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) { @@ -63,13 +62,13 @@ export abstract class PlatformLiveSyncServiceBase { const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); const localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, null, []); - const modifiedFilesData = await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, projectData, syncInfo.liveSyncDeviceInfo, { isFullSync: true, force: syncInfo.force }); + const modifiedFilesData = await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, projectData, syncInfo.liveSyncDeviceData, { isFullSync: true, force: syncInfo.force }); return { modifiedFilesData, isFullSync: true, deviceAppData, - useHotModuleReload: syncInfo.useHotModuleReload + useHotModuleReload: syncInfo.useHotModuleReload, }; } @@ -86,32 +85,32 @@ 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, []); modifiedLocalToDevicePaths.push(...localToDevicePaths); - modifiedLocalToDevicePaths = await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, projectData, liveSyncInfo.liveSyncDeviceInfo, { isFullSync: false, force: liveSyncInfo.force }); + modifiedLocalToDevicePaths = await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, projectData, liveSyncInfo.liveSyncDeviceData, { isFullSync: false, force: liveSyncInfo.force }); } } 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); @@ -129,11 +128,11 @@ export abstract class PlatformLiveSyncServiceBase { }; } - protected async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo, options: ITransferFilesOptions): Promise { + protected async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceData: ILiveSyncDeviceDescriptor, options: ITransferFilesOptions): Promise { let transferredFiles: Mobile.ILocalToDevicePathData[] = []; const deviceLiveSyncService = this.getDeviceLiveSyncService(deviceAppData.device, projectData); - transferredFiles = await deviceLiveSyncService.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, projectData, liveSyncDeviceInfo, options); + transferredFiles = await deviceLiveSyncService.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, projectData, liveSyncDeviceData, options); this.logFilesSyncInformation(transferredFiles, "Successfully transferred %s on device %s.", this.$logger.info, deviceAppData.device.deviceInfo.identifier); 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 deleted file mode 100644 index 1b18380916..0000000000 --- a/lib/services/livesync/playground/preview-app-livesync-service.ts +++ /dev/null @@ -1,196 +0,0 @@ -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 { EventEmitter } from "events"; -import { performanceLog } from "../../../common/decorators"; - -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 $previewAppFilesService: IPreviewAppFilesService, - private $previewAppPluginsService: IPreviewAppPluginsService, - private $previewDevicesService: IPreviewDevicesService, - private $hmrStatusService: IHmrStatusService, - protected $workflowService: IWorkflowService) { - super(); - } - - @performanceLog() - public async initialize(data: IPreviewAppLiveSyncData): Promise { - await this.$previewSdkService.initialize(data.projectDir, async (device: Device) => { - await this.$workflowService.handleLegacyWorkflow({ projectDir: data.projectDir, settings: data }); - 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 - }); - } - }); - } - - @performanceLog() - public async syncFiles(data: IPreviewAppLiveSyncData, filesToSync: string[], filesToRemove: string[]): Promise { - this.showWarningsForNativeFiles(filesToSync); - - const connectedDevices = this.$previewDevicesService.getConnectedDevices(); - for (const device of connectedDevices) { - await this.$previewAppPluginsService.comparePluginsOnDevice(data, device); - } - - const platforms = _(connectedDevices) - .map(device => device.platform) - .uniq() - .value(); - - for (const platform of platforms) { - await this.syncFilesForPlatformSafe(data, { filesToSync, filesToRemove }, platform); - } - } - - 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 { - try { - const payloads = this.$previewAppFilesService.getFilesPayload(data, filesData, platform); - if (payloads && payloads.files && payloads.files.length) { - this.$logger.info(`Start syncing changes for platform ${platform}.`); - await this.$previewSdkService.applyChanges(payloads); - this.$logger.info(`Successfully synced ${payloads.files.map(filePayload => filePayload.file.yellow)} for platform ${platform}.`); - } - } 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 - }); - } - } - - @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.`)); - } -} -$injector.register("previewAppLiveSyncService", PreviewAppLiveSyncService); 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 b33f49051d..0000000000 --- a/lib/services/local-build-service.ts +++ /dev/null @@ -1,54 +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, platformTemplate?: string): 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, - platformTemplate, - 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..d7bdadd84f 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, @@ -15,9 +14,8 @@ export class PlatformEnvironmentRequirements implements IPlatformEnvironmentRequ private $injector: IInjector, private $previewQrCodeService: IPreviewQrCodeService) { } - @cache() - private get $liveSyncService(): ILiveSyncService { - return this.$injector.resolve("liveSyncService"); + public get $previewAppController(): IPreviewAppController { + return this.$injector.resolve("previewAppController"); } public static CLOUD_SETUP_OPTION_NAME = "Configure for Cloud Builds"; @@ -181,17 +179,10 @@ 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, + await this.$previewAppController.startPreview({ 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 + useHotModuleReload: options.hmr, }); 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 500ca4df7a..0000000000 --- a/lib/services/platform-service.ts +++ /dev/null @@ -1,988 +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, - public $workflowService: IWorkflowService - ) { - super(); - } - - public async cleanPlatforms(platforms: string[], platformTemplate: 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], platformTemplate, projectData, config); - } - } - - public async addPlatforms(platforms: string[], platformTemplate: 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(), platformTemplate, 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, platformTemplate: 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.info("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, platformTemplate, projectData, config, nativePrepare); - } catch (err) { - this.$fs.deleteDirectory(platformPath); - throw err; - } finally { - spinner.stop(); - } - - this.$fs.ensureDirectoryExists(platformPath); - this.$logger.info(`Platform ${platform} successfully added. v${installedPlatformVersion}`); - } - - private async addPlatformCore(platformData: IPlatformData, frameworkDir: string, platformTemplate: 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, - platformTemplate - }); - - 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 { - await this.$workflowService.handleLegacyWorkflow({ projectDir: platformInfo.projectData.projectDir, settings: platformInfo.appFilesUpdaterOptions }); - 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.info("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, platformTemplate, 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, platformTemplate, 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.info("Preparing project..."); - - const platformData = this.$platformsData.getPlatformData(platform, projectData); - const frameworkVersion = this.getCurrentPlatformVersion(platform, projectData); - if (semver.lt(semver.coerce(frameworkVersion), semver.coerce('5.1.0'))) { - this.$logger.warn(`Runtime versions lower than 5.1.0 have been deprecated and will not be supported as of v6.0.0 of NativeScript CLI. More info can be found in this issue https://github.com/NativeScript/nativescript-cli/issues/4518.`); - } - - 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.info(`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.info("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.info(data.data.toString(), { [constants.LoggerConfigData.skipNewLine]: true }); - }; - - 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.info("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.info(`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.info(`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, - platformTemplate: deployInfo.deployOptions.platformTemplate, - 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.info("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.info("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.info("Starting..."); - - const action = async (device: Mobile.IDevice) => { - await device.applicationManager.startApplication(appData); - this.$logger.info(`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.platformTemplate, 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 gradleErrorMessage; - - try { - await platformData.platformProjectService.stopServices(platformData.projectRoot); - } catch (err) { - gradleErrorMessage = err.message; - } - - try { - const platformDir = path.join(projectData.platformsDir, platform.toLowerCase()); - this.$fs.deleteDirectory(platformDir); - this.$projectDataService.removeNSProperty(projectData.projectDir, platformData.frameworkPackageName); - - this.$logger.info(`Platform ${platform} successfully removed.`); - } catch (err) { - this.$logger.error(`Failed to remove ${platform} platform with errors:`); - if (gradleErrorMessage) { - this.$logger.error(gradleErrorMessage); - } - this.$errors.failWithoutHelp(err.message); - } - } - } - - public async updatePlatforms(platforms: string[], platformTemplate: 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, platformTemplate, projectData, config); - } else { - await this.addPlatform(platformParam, platformTemplate, 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.isValidPlatform(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, platformTemplate: 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, platformTemplate, 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, platformTemplate, projectData, config, "", nativePrepare); - } - } else { - await this.addPlatform(platform, platformTemplate, 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, platformTemplate: 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, platformTemplate, 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())); - } - - private isValidPlatform(platform: string, projectData: IProjectData) { - return this.$platformsData.getPlatformData(platform, projectData); - } - - 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, platformTemplate: 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, platformTemplate }, 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, updateOptions.platformTemplate, projectData, config); - this.$logger.info("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..e97e3b801b --- /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); + await this.$projectChangesService.setNativePlatformStatus(platformData, projectData, { 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..6b3549cb3f --- /dev/null +++ b/lib/services/platform/prepare-native-platform-service.ts @@ -0,0 +1,74 @@ + +import { hook } from "../../common/helpers"; +import { performanceLog } from "../../common/decorators"; +import { NativePlatformStatus } from "../../constants"; + +export class PrepareNativePlatformService implements IPrepareNativePlatformService { + + constructor( + 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 hasNativeModulesChange = !changesInfo || changesInfo.nativeChanged; + const hasConfigChange = !changesInfo || changesInfo.configChanged; + const hasChangesRequirePrepare = !changesInfo || changesInfo.changesRequirePrepare; + + const hasChanges = hasNativeModulesChange || hasConfigChange || hasChangesRequirePrepare; + + if (changesInfo.hasChanges) { + await this.cleanProject(platformData, { release }); + } + + platformData.platformProjectService.prepareAppResources(projectData); + + if (hasChangesRequirePrepare) { + await platformData.platformProjectService.prepareProject(projectData, prepareData); + } + + if (hasNativeModulesChange) { + await this.$nodeModulesBuilder.prepareNodeModules(platformData, projectData); + } + + if (hasNativeModulesChange || hasConfigChange) { + await platformData.platformProjectService.processConfigurationFilesFromAppResources(projectData, { release }); + await platformData.platformProjectService.handleNativeDependenciesChange(projectData, { release }); + } + + platformData.platformProjectService.interpolateConfigurationFile(projectData); + await this.$projectChangesService.setNativePlatformStatus(platformData, projectData, { nativePlatformStatus: NativePlatformStatus.alreadyPrepared }); + + return hasChanges; + } + + private async cleanProject(platformData: IPlatformData, 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); + } + } +} +$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/plugin-variables-service.ts b/lib/services/plugin-variables-service.ts deleted file mode 100644 index d73ed0a56d..0000000000 --- a/lib/services/plugin-variables-service.ts +++ /dev/null @@ -1,104 +0,0 @@ -import * as helpers from "./../common/helpers"; - -export class PluginVariablesService implements IPluginVariablesService { - private static PLUGIN_VARIABLES_KEY = "variables"; - - constructor(private $errors: IErrors, - private $pluginVariablesHelper: IPluginVariablesHelper, - private $projectDataService: IProjectDataService, - private $prompter: IPrompter, - private $fs: IFileSystem) { } - - public getPluginVariablePropertyName(pluginName: string): string { - return `${pluginName}-${PluginVariablesService.PLUGIN_VARIABLES_KEY}`; - } - - public async savePluginVariablesInProjectFile(pluginData: IPluginData, projectDir: string): Promise { - const values = Object.create(null); - await this.executeForAllPluginVariables(pluginData, async (pluginVariableData: IPluginVariableData) => { - const pluginVariableValue = await this.getPluginVariableValue(pluginVariableData); - this.ensurePluginVariableValue(pluginVariableValue, `Unable to find value for ${pluginVariableData.name} plugin variable from ${pluginData.name} plugin. Ensure the --var option is specified or the plugin variable has default value.`); - values[pluginVariableData.name] = pluginVariableValue; - }, projectDir); - - if (!_.isEmpty(values)) { - this.$projectDataService.setNSValue(projectDir, this.getPluginVariablePropertyName(pluginData.name), values); - } - } - - public removePluginVariablesFromProjectFile(pluginName: string, projectDir: string): void { - this.$projectDataService.removeNSProperty(projectDir, this.getPluginVariablePropertyName(pluginName)); - } - - public async interpolatePluginVariables(pluginData: IPluginData, pluginConfigurationFilePath: string, projectDir: string): Promise { - let pluginConfigurationFileContent = this.$fs.readText(pluginConfigurationFilePath); - await this.executeForAllPluginVariables(pluginData, async (pluginVariableData: IPluginVariableData) => { - this.ensurePluginVariableValue(pluginVariableData.value, `Unable to find the value for ${pluginVariableData.name} plugin variable into project package.json file. Verify that your package.json file is correct and try again.`); - pluginConfigurationFileContent = this.interpolateCore(pluginVariableData.name, pluginVariableData.value, pluginConfigurationFileContent); - }, projectDir); - - this.$fs.writeFile(pluginConfigurationFilePath, pluginConfigurationFileContent); - } - - public interpolateAppIdentifier(pluginConfigurationFilePath: string, projectIdentifier: string): void { - const pluginConfigurationFileContent = this.$fs.readText(pluginConfigurationFilePath); - const newContent = this.interpolateCore("nativescript.id", projectIdentifier, pluginConfigurationFileContent); - this.$fs.writeFile(pluginConfigurationFilePath, newContent); - } - - public async interpolate(pluginData: IPluginData, pluginConfigurationFilePath: string, projectDir: string, projectIdentifier: string): Promise { - await this.interpolatePluginVariables(pluginData, pluginConfigurationFilePath, projectDir); - this.interpolateAppIdentifier(pluginConfigurationFilePath, projectIdentifier); - } - - private interpolateCore(name: string, value: string, content: string): string { - return content.replace(new RegExp(`{${name}}`, "gi"), value); - } - - private ensurePluginVariableValue(pluginVariableValue: string, errorMessage: string): void { - if (!pluginVariableValue) { - this.$errors.failWithoutHelp(errorMessage); - } - } - - private async getPluginVariableValue(pluginVariableData: IPluginVariableData): Promise { - const pluginVariableName = pluginVariableData.name; - let value = this.$pluginVariablesHelper.getPluginVariableFromVarOption(pluginVariableName); - if (value) { - value = value[pluginVariableName]; - } else { - value = pluginVariableData.defaultValue; - if (!value && helpers.isInteractive()) { - const promptSchema = { - name: pluginVariableName, - type: "input", - message: `Enter value for ${pluginVariableName} variable:`, - validate: (val: string) => !!val ? true : 'Please enter a value!' - }; - const promptData = await this.$prompter.get([promptSchema]); - value = promptData[pluginVariableName]; - } - } - - return value; - } - - private async executeForAllPluginVariables(pluginData: IPluginData, action: (pluginVariableData: IPluginVariableData) => Promise, projectDir: string): Promise { - const pluginVariables = pluginData.pluginVariables; - const pluginVariablesNames = _.keys(pluginVariables); - await Promise.all(_.map(pluginVariablesNames, pluginVariableName => action(this.createPluginVariableData(pluginData, pluginVariableName, projectDir)))); - } - - private createPluginVariableData(pluginData: IPluginData, pluginVariableName: string, projectDir: string): IPluginVariableData { - const variableData = pluginData.pluginVariables[pluginVariableName]; - - variableData.name = pluginVariableName; - - const pluginVariableValues = this.$projectDataService.getNSValue(projectDir, this.getPluginVariablePropertyName(pluginData.name)); - variableData.value = pluginVariableValues ? pluginVariableValues[pluginVariableName] : undefined; - - return variableData; - } -} - -$injector.register("pluginVariablesService", PluginVariablesService); diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index 857e05dbbc..99bb566ac0 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -9,18 +9,12 @@ export class PluginsService implements IPluginsService { private static NPM_CONFIG = { save: true }; - private get $platformsData(): IPlatformsData { - return this.$injector.resolve("platformsData"); - } - private get $pluginVariablesService(): IPluginVariablesService { - return this.$injector.resolve("pluginVariablesService"); + private get $platformsDataService(): IPlatformsDataService { + return this.$injector.resolve("platformsDataService"); } private get $projectDataService(): IProjectDataService { return this.$injector.resolve("projectDataService"); } - private get $projectFilesManager(): IProjectFilesManager { - return this.$injector.resolve("projectFilesManager"); - } private get npmInstallOptions(): INodePackageManagerInstallOptions { return _.merge({ @@ -37,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); @@ -60,16 +55,6 @@ export class PluginsService implements IPluginsService { await this.executeForAllInstalledPlatforms(action, projectData); - try { - await this.$pluginVariablesService.savePluginVariablesInProjectFile(pluginData, projectData.projectDir); - } catch (err) { - // Revert package.json - this.$projectDataService.removeNSProperty(projectData.projectDir, this.$pluginVariablesService.getPluginVariablePropertyName(pluginData.name)); - await this.$packageManager.uninstall(plugin, PluginsService.NPM_CONFIG, projectData.projectDir); - - throw err; - } - this.$logger.info(`Successfully installed plugin ${realNpmPackageJson.name}.`); } else { await this.$packageManager.uninstall(realNpmPackageJson.name, { save: true }, projectData.projectDir); @@ -84,7 +69,6 @@ export class PluginsService implements IPluginsService { await platformData.platformProjectService.removePluginNativeCode(pluginData, projectData); }; - this.$pluginVariablesService.removePluginVariablesFromProjectFile(pluginName.toLowerCase(), projectData.projectDir); await this.executeForAllInstalledPlatforms(removePluginNativeCodeAction, projectData); await this.executeNpmCommand(PluginsService.UNINSTALL_COMMAND_NAME, pluginName, projectData); @@ -104,44 +88,8 @@ export class PluginsService implements IPluginsService { } } - public async validate(platformData: IPlatformData, projectData: IProjectData): Promise { - return await platformData.platformProjectService.validatePlugins(projectData); - } - - public async prepare(dependencyData: IDependencyData, platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise { - platform = platform.toLowerCase(); - const platformData = this.$platformsData.getPlatformData(platform, projectData); - const pluginData = this.convertToPluginData(dependencyData, projectData.projectDir); - - const appFolderExists = this.$fs.exists(path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME)); - if (appFolderExists) { - this.preparePluginScripts(pluginData, platform, projectData, projectFilesConfig); - await this.preparePluginNativeCode(pluginData, platform, projectData); - - // Show message - this.$logger.info(`Successfully prepared plugin ${pluginData.name} for ${platform}.`); - } - } - - public preparePluginScripts(pluginData: IPluginData, platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): void { - const platformData = this.$platformsData.getPlatformData(platform, projectData); - const pluginScriptsDestinationPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME, "tns_modules"); - const scriptsDestinationExists = this.$fs.exists(pluginScriptsDestinationPath); - if (!scriptsDestinationExists) { - //tns_modules/ doesn't exist. Assuming we're running a bundled prepare. - return; - } - - if (!this.isPluginDataValidForPlatform(pluginData, platform, projectData)) { - return; - } - - //prepare platform speciffic files, .map and .ts files - this.$projectFilesManager.processPlatformSpecificFiles(pluginScriptsDestinationPath, platform, projectFilesConfig); - } - 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); @@ -261,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; } @@ -295,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); } @@ -307,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; } @@ -316,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 versionRequiredByPlugin = (pluginPlatformsData)[platform]; if (!versionRequiredByPlugin) { 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 6b74a4e262..0000000000 --- a/lib/services/prepare-platform-js-service.ts +++ /dev/null @@ -1,120 +0,0 @@ -import * as constants from "../constants"; -import * as path from "path"; -import * as shell from "shelljs"; -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 $errors: IErrors, - private $logger: ILogger, - private $projectDataService: IProjectDataService, - private $nodeModulesBuilder: INodeModulesBuilder, - private $packageManager: INodePackageManager) { - super($fs, $hooksService, $xmlValidator); - } - - public async addPlatform(info: IAddPlatformInfo): Promise { - const customTemplateOptions = await this.getPathToPlatformTemplate(info.platformTemplate, info.platformData.frameworkPackageName, info.projectData.projectDir); - info.config.pathToTemplate = customTemplateOptions && customTemplateOptions.pathToTemplate; - - const frameworkPackageNameData: any = { version: info.installedVersion }; - if (customTemplateOptions) { - frameworkPackageNameData.template = customTemplateOptions.selectedTemplate; - } - - this.$projectDataService.setNSValue(info.projectData.projectDir, info.platformData.frameworkPackageName, frameworkPackageNameData); - } - - @performanceLog() - @hook('prepareJSApp') - public async preparePlatform(config: IPreparePlatformJSInfo): Promise { - if (!config.changesInfo || config.changesInfo.appFilesChanged || config.changesInfo.changesRequirePrepare) { - await this.copyAppFiles(config); - this.copyAppResourcesFiles(config); - } - - if (config.changesInfo && !config.changesInfo.changesRequirePrepare) { - // remove the App_Resources folder from the app/assets as here we're applying other files changes. - const appDestinationDirectoryPath = path.join(config.platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); - const appResourcesDirectoryPath = path.join(appDestinationDirectoryPath, path.basename(config.projectData.appResourcesDirectoryPath)); - if (this.$fs.exists(appResourcesDirectoryPath)) { - this.$fs.deleteDirectory(appResourcesDirectoryPath); - } - } - - if (!config.changesInfo || config.changesInfo.modulesChanged) { - await this.copyTnsModules(config.platform, config.platformData, config.projectData, config.appFilesUpdaterOptions, config.projectFilesConfig); - } - } - - private async getPathToPlatformTemplate(selectedTemplate: string, frameworkPackageName: string, projectDir: string): Promise<{ selectedTemplate: string, pathToTemplate: string }> { - if (!selectedTemplate) { - // read data from package.json's nativescript key - // check the nativescript.tns-.template value - const nativescriptPlatformData = this.$projectDataService.getNSValue(projectDir, frameworkPackageName); - selectedTemplate = nativescriptPlatformData && nativescriptPlatformData.template; - } - - if (selectedTemplate) { - const tempDir = temp.mkdirSync("platform-template"); - this.$fs.writeJson(path.join(tempDir, constants.PACKAGE_JSON_FILE_NAME), {}); - try { - const npmInstallResult = await this.$packageManager.install(selectedTemplate, tempDir, { - disableNpmInstall: false, - frameworkPath: null, - ignoreScripts: false - }); - const pathToTemplate = path.join(tempDir, constants.NODE_MODULES_FOLDER_NAME, npmInstallResult.name); - return { selectedTemplate, pathToTemplate }; - } catch (err) { - this.$logger.trace("Error while trying to install specified template: ", err); - this.$errors.failWithoutHelp(`Unable to install platform template ${selectedTemplate}. Make sure the specified value is valid.`); - } - } - - return null; - } - - private async copyTnsModules(platform: string, platformData: IPlatformData, projectData: IProjectData, appFilesUpdaterOptions: IAppFilesUpdaterOptions, projectFilesConfig?: IProjectFilesConfig): Promise { - const appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); - const lastModifiedTime = this.$fs.exists(appDestinationDirectoryPath) ? this.$fs.getFsStats(appDestinationDirectoryPath).mtime : null; - - try { - const absoluteOutputPath = path.join(appDestinationDirectoryPath, constants.TNS_MODULES_FOLDER_NAME); - // Process node_modules folder - await this.$nodeModulesBuilder.prepareJSNodeModules({ - nodeModulesData: { - absoluteOutputPath, - platform, - lastModifiedTime, - projectData, - appFilesUpdaterOptions, - projectFilesConfig - }, - release: appFilesUpdaterOptions.release, - copyNodeModules: true - }); - } catch (error) { - this.$logger.debug(error); - shell.rm("-rf", appDestinationDirectoryPath); - this.$errors.failWithoutHelp(`Processing node_modules failed. ${error}`); - } - } - - private copyAppResourcesFiles(config: IPreparePlatformJSInfo): void { - const appDestinationDirectoryPath = path.join(config.platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); - const appResourcesSourcePath = config.projectData.appResourcesDirectoryPath; - - shell.cp("-Rf", appResourcesSourcePath, path.join(appDestinationDirectoryPath, constants.APP_RESOURCES_FOLDER_NAME)); - } -} - -$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 4de09e99a7..0000000000 --- a/lib/services/prepare-platform-native-service.ts +++ /dev/null @@ -1,135 +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 $pluginsService: IPluginsService, - 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) { - await this.$pluginsService.validate(config.platformData, config.projectData); - - 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..c539525cc7 100644 --- a/lib/services/project-changes-service.ts +++ b/lib/services/project-changes-service.ts @@ -1,33 +1,27 @@ 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 { NativePlatformStatus, PACKAGE_JSON_FILE_NAME, APP_GRADLE_FILE_NAME, BUILD_XCCONFIG_FILE_NAME, PLATFORMS_DIR_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 || + return this.nativeChanged || this.appResourcesChanged || - this.modulesChanged || this.configChanged || this.signingChanged; } public get changesRequireBuild(): boolean { - return this.packageChanged || - this.appResourcesChanged || + return this.appResourcesChanged || this.nativeChanged; } @@ -41,17 +35,15 @@ export class ProjectChangesService implements IProjectChangesService { private _changesInfo: IProjectChangesInfo; private _prepareInfo: IPrepareInfo; - private _newFiles: number = 0; private _outputProjectMtime: number; private _outputProjectCTime: number; constructor( - private $platformsData: IPlatformsData, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $fs: IFileSystem, - private $filesHashService: IFilesHashService, private $logger: ILogger, - public $hooksService: IHooksService) { + public $hooksService: IHooksService, + private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder) { } public get currentChanges(): IProjectChangesInfo { @@ -59,35 +51,29 @@ 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); - 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( - 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}`); - - 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; + this._changesInfo.appResourcesChanged = this.containsNewerFiles(platformResourcesDir, projectData); + + this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir) + .filter(dep => dep.nativescript && this.$fs.exists(path.join(dep.directory, PLATFORMS_DIR_NAME, platformData.platformNameLowerCase))) + .forEach(dep => { + this._changesInfo.nativeChanged = this._changesInfo.nativeChanged || + this.containsNewerFiles(path.join(dep.directory, PLATFORMS_DIR_NAME, platformData.platformNameLowerCase), projectData) || + this.isFileModified(path.join(dep.directory, PACKAGE_JSON_FILE_NAME)); + }); + + if (!this._changesInfo.nativeChanged) { + this._prepareInfo.projectFileHash = this.getProjectFileStrippedHash(projectData.projectDir, platformData); + this._changesInfo.nativeChanged = this.isProjectFileChanged(projectData.projectDir, platformData); } - if (platform === this.$devicePlatformsConstants.iOS.toLowerCase()) { + this.$logger.trace(`Set nativeChanged to ${this._changesInfo.nativeChanged}.`); + + 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,27 +88,18 @@ 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"); - this._changesInfo.modulesChanged = true; - } - if (this._changesInfo.modulesChanged || this._changesInfo.appResourcesChanged) { - this.$logger.trace(`Set configChanged to true, current value of moduleChanged is: ${this._changesInfo.modulesChanged}, appResourcesChanged is: ${this._changesInfo.appResourcesChanged}`); + if (this._changesInfo.appResourcesChanged) { + this.$logger.trace(`Set configChanged to true, appResourcesChanged is: ${this._changesInfo.appResourcesChanged}`); this._changesInfo.configChanged = true; } if (this._changesInfo.hasChanges) { @@ -131,8 +108,6 @@ export class ProjectChangesService implements IProjectChangesService { if (this._prepareInfo.changesRequireBuild) { this._prepareInfo.changesRequireBuildTime = this._prepareInfo.time; } - - this._prepareInfo.projectFileHash = this.getProjectFileStrippedHash(projectData, platform); } this._changesInfo.nativePlatformStatus = this._prepareInfo.nativePlatformStatus; @@ -141,14 +116,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 +132,21 @@ export class ProjectChangesService implements IProjectChangesService { prepareInfo = null; } } + return prepareInfo; } - public savePrepareInfo(platform: string, projectData: IProjectData): void { - const prepareInfoFilePath = this.getPrepareInfoFilePath(platform, projectData); + public async savePrepareInfo(platformData: IPlatformData, projectData: IProjectData, prepareData: IPrepareData): Promise { + if (!this._prepareInfo) { + await this.ensurePrepareInfo(platformData, projectData, prepareData); + } + + 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 async setNativePlatformStatus(platformData: IPlatformData, projectData: IProjectData, addedPlatform: IAddedNativePlatform): Promise { + this._prepareInfo = this._prepareInfo || this.getPrepareInfo(platformData); if (this._prepareInfo && addedPlatform.nativePlatformStatus === NativePlatformStatus.alreadyPrepared) { this._prepareInfo.nativePlatformStatus = addedPlatform.nativePlatformStatus; } else { @@ -175,52 +155,45 @@ export class ProjectChangesService implements IProjectChangesService { }; } - this.savePrepareInfo(platform, projectData); + await this.savePrepareInfo(platformData, projectData, null); } - 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; + this._changesInfo.nativeChanged = 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 +201,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; } @@ -247,7 +220,7 @@ export class ProjectChangesService implements IProjectChangesService { return false; } - private containsNewerFiles(dir: string, skipDir: string, projectData: IProjectData, processFunc?: (filePath: string, projectData: IProjectData) => boolean): boolean { + private containsNewerFiles(dir: string, projectData: IProjectData): boolean { const dirName = path.basename(dir); this.$logger.trace(`containsNewerFiles will check ${dir}`); if (_.startsWith(dirName, '.')) { @@ -255,8 +228,7 @@ export class ProjectChangesService implements IProjectChangesService { return false; } - const dirFileStat = this.$fs.getFsStats(dir); - if (this.isFileModified(dirFileStat, dir)) { + if (this.isFileModified(dir)) { this.$logger.trace(`containsNewerFiles returns true for ${dir} as the dir itself has been modified.`); return true; } @@ -264,31 +236,17 @@ export class ProjectChangesService implements IProjectChangesService { const files = this.$fs.readDirectory(dir); for (const file of files) { const filePath = path.join(dir, file); - if (filePath === skipDir) { - continue; - } const fileStats = this.$fs.getFsStats(filePath); - const changed = this.isFileModified(fileStats, filePath); + const changed = this.isFileModified(filePath, fileStats); if (changed) { - this.$logger.trace(`File ${filePath} has been changed.`); - if (processFunc) { - this._newFiles++; - this.$logger.trace(`Incremented the newFiles counter. Current value is ${this._newFiles}`); - const filePathRelative = path.relative(projectData.projectDir, filePath); - if (processFunc.call(this, filePathRelative, projectData)) { - this.$logger.trace(`containsNewerFiles returns true for ${dir}. The modified file is ${filePath}`); - return true; - } - } else { - this.$logger.trace(`containsNewerFiles returns true for ${dir}. The modified file is ${filePath}`); - return true; - } + this.$logger.trace(`containsNewerFiles returns true for ${dir}. The modified file is ${filePath}`); + return true; } if (fileStats.isDirectory()) { - if (this.containsNewerFiles(filePath, skipDir, projectData, processFunc)) { + if (this.containsNewerFiles(filePath, projectData)) { this.$logger.trace(`containsNewerFiles returns true for ${dir}.`); return true; } @@ -299,9 +257,10 @@ export class ProjectChangesService implements IProjectChangesService { return false; } - private isFileModified(filePathStat: IFsStats, filePath: string): boolean { - let changed = filePathStat.mtime.getTime() >= this._outputProjectMtime || - filePathStat.ctime.getTime() >= this._outputProjectCTime; + private isFileModified(filePath: string, filePathStats?: IFsStats): boolean { + filePathStats = filePathStats || this.$fs.getFsStats(filePath); + let changed = filePathStats.mtime.getTime() >= this._outputProjectMtime || + filePathStats.ctime.getTime() >= this._outputProjectCTime; if (!changed) { const lFileStats = this.$fs.getLsStats(filePath); @@ -311,44 +270,5 @@ export class ProjectChangesService implements IProjectChangesService { return changed; } - - private fileChangeRequiresBuild(file: string, projectData: IProjectData) { - if (path.basename(file) === PACKAGE_JSON_FILE_NAME) { - return true; - } - const projectDir = projectData.projectDir; - if (_.startsWith(path.join(projectDir, file), projectData.appResourcesDirectoryPath)) { - return true; - } - if (_.startsWith(file, NODE_MODULES_FOLDER_NAME)) { - let filePath = file; - while (filePath !== NODE_MODULES_FOLDER_NAME) { - filePath = path.dirname(filePath); - const fullFilePath = path.join(projectDir, path.join(filePath, PACKAGE_JSON_FILE_NAME)); - if (this.$fs.exists(fullFilePath)) { - const json = this.$fs.readJson(fullFilePath); - if (json["nativescript"] && _.startsWith(file, path.join(filePath, "platforms"))) { - return true; - } - } - } - } - 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/project-data-service.ts b/lib/services/project-data-service.ts index 8797993198..6304fd952b 100644 --- a/lib/services/project-data-service.ts +++ b/lib/services/project-data-service.ts @@ -1,5 +1,4 @@ import * as path from "path"; -import * as constants from "../constants"; import { ProjectData } from "../project-data"; import { exported } from "../common/decorators"; import { @@ -139,13 +138,6 @@ export class ProjectDataService implements IProjectDataService { }; } - public setUseLegacyWorkflow(projectDir: string, value: any): void { - this.$logger.trace(`useLegacyWorkflow will be set to ${value}`); - this.updateNsConfigValue(projectDir, { useLegacyWorkflow: value }); - this.refreshProjectData(projectDir); - this.$logger.trace(`useLegacyWorkflow was set to ${value}`); - } - public getAppExecutableFiles(projectDir: string): string[] { const projectData = this.getProjectData(projectDir); @@ -179,33 +171,6 @@ export class ProjectDataService implements IProjectDataService { return files; } - private refreshProjectData(projectDir: string) { - if (this.projectDataCache[projectDir]) { - this.projectDataCache[projectDir].initializeProjectData(projectDir); - } - } - - private updateNsConfigValue(projectDir: string, updateObject: INsConfig): void { - const nsConfigPath = path.join(projectDir, constants.CONFIG_NS_FILE_NAME); - const currentNsConfig = this.getNsConfig(nsConfigPath); - const newNsConfig = Object.assign(currentNsConfig, updateObject); - - this.$fs.writeJson(nsConfigPath, newNsConfig); - } - - private getNsConfig(nsConfigPath: string): INsConfig { - let result = this.getNsConfigDefaultObject(); - if (this.$fs.exists(nsConfigPath)) { - try { - result = this.$fs.readJson(nsConfigPath); - } catch (e) { - // default - } - } - - return result; - } - private getImageDefinitions(): IImageDefinitionsStructure { const pathToImageDefinitions = path.join(__dirname, "..", "..", CLI_RESOURCES_DIR_NAME, AssetConstants.assets, AssetConstants.imageDefinitionsFileName); const imageDefinitions = this.$fs.readJson(pathToImageDefinitions); diff --git a/lib/services/project-init-service.ts b/lib/services/project-init-service.ts deleted file mode 100644 index 24937a1f5c..0000000000 --- a/lib/services/project-init-service.ts +++ /dev/null @@ -1,133 +0,0 @@ -import * as constants from "../constants"; -import * as helpers from "../common/helpers"; -import * as path from "path"; -import * as semver from "semver"; - -export class ProjectInitService implements IProjectInitService { - private static MIN_SUPPORTED_FRAMEWORK_VERSIONS: IStringDictionary = { - "tns-ios": "1.1.0", - "tns-android": "1.1.0", - "tns-core-modules": "1.2.0" - }; - - private static VERSION_KEY_NAME = "version"; - - private _projectFilePath: string; - - constructor(private $fs: IFileSystem, - private $logger: ILogger, - private $options: IOptions, - private $injector: IInjector, - private $staticConfig: IStaticConfig, - private $projectHelper: IProjectHelper, - private $prompter: IPrompter, - private $packageManager: INodePackageManager, - private $packageInstallationManager: IPackageInstallationManager) { } - - public async initialize(): Promise { - let projectData: any = {}; - - if (this.$fs.exists(this.projectFilePath)) { - projectData = this.$fs.readJson(this.projectFilePath); - } - - const projectDataBackup = _.extend({}, projectData); - - 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 - } - - try { - projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE]["id"] = await this.getProjectId(); - - if (this.$options.frameworkName && this.$options.frameworkVersion) { - const currentPlatformData = projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][this.$options.frameworkName] || {}; - - 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 $projectData = this.$injector.resolve("projectData"); - $projectData.initializeProjectData(path.dirname(this.projectFilePath)); - for (const platform of $platformsData.platformsNames) { - const platformData: IPlatformData = $platformsData.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] || {}; - - projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][platformData.frameworkPackageName] = _.extend(currentPlatformData, await this.getVersionData(platformData.frameworkPackageName)); - } - } - } - - const dependencies = projectData.dependencies; - if (!dependencies) { - projectData.dependencies = Object.create(null); - } - - // In case console is interactive and --force is not specified, do not read the version from package.json, show all available versions to the user. - const tnsCoreModulesVersionInPackageJson = this.useDefaultValue ? projectData.dependencies[constants.TNS_CORE_MODULES_NAME] : null; - projectData.dependencies[constants.TNS_CORE_MODULES_NAME] = tnsCoreModulesVersionInPackageJson || (await this.getVersionData(constants.TNS_CORE_MODULES_NAME))["version"]; - - this.$fs.writeJson(this.projectFilePath, projectData); - } catch (err) { - this.$fs.writeJson(this.projectFilePath, projectDataBackup); - throw err; - } - - this.$logger.info("Project successfully initialized."); - } - - private get projectFilePath(): string { - if (!this._projectFilePath) { - const projectDir = path.resolve(this.$options.path || "."); - this._projectFilePath = path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME); - } - - return this._projectFilePath; - } - - private async getProjectId(): Promise { - if (this.$options.appid) { - return this.$options.appid; - } - - const defaultAppId = this.$projectHelper.generateDefaultAppId(path.basename(path.dirname(this.projectFilePath)), constants.DEFAULT_APP_IDENTIFIER_PREFIX); - if (this.useDefaultValue) { - return defaultAppId; - } - - return await this.$prompter.getString("Id:", { defaultAction: () => defaultAppId }); - } - - private async getVersionData(packageName: string): Promise { - const latestVersion = await this.$packageInstallationManager.getLatestCompatibleVersion(packageName); - - if (this.useDefaultValue) { - return this.buildVersionData(latestVersion); - } - - const allVersions: any = await this.$packageManager.view(packageName, { "versions": true }); - const versions = _.filter(allVersions, (v: string) => semver.gte(v, ProjectInitService.MIN_SUPPORTED_FRAMEWORK_VERSIONS[packageName])); - if (versions.length === 1) { - this.$logger.info(`Only ${versions[0]} version is available for ${packageName}.`); - return this.buildVersionData(versions[0]); - } - const sortedVersions = versions.sort(helpers.versionCompare).reverse(); - //TODO: plamen5kov: don't offer versions from next (they are not available) - const version = await this.$prompter.promptForChoice(`${packageName} version:`, sortedVersions); - return this.buildVersionData(version); - } - - private buildVersionData(version: string): IStringDictionary { - const result: IStringDictionary = {}; - - result[ProjectInitService.VERSION_KEY_NAME] = version; - - return result; - } - - private get useDefaultValue(): boolean { - return !helpers.isInteractive() || this.$options.force; - } -} -$injector.register("projectInitService", ProjectInitService); diff --git a/lib/services/test-execution-service.ts b/lib/services/test-execution-service.ts index b8fc54f05b..9f9ebb4728 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,54 @@ 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: ILiveSyncDeviceDescriptor[]): 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({ + liveSyncInfo, + deviceDescriptors + }); }; karmaRunner.on("message", (karmaData: any) => { @@ -198,7 +135,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 +155,7 @@ export class TestExecutionService implements ITestExecutionService { } karmaConfig.projectDir = projectData.projectDir; - karmaConfig.bundle = this.$options.bundle; + karmaConfig.bundle = true; karmaConfig.debugBrk = this.$options.debugBrk; 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..693345ae84 --- /dev/null +++ b/lib/services/webpack/webpack-compiler-service.ts @@ -0,0 +1,237 @@ +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, + public $hostInfo: IHostInfo, + private $logger: ILogger, + private $pluginsService: IPluginsService, + private $mobileHelper: Mobile.IMobileHelper + ) { super(); } + + public async compileWithWatch(platformData: IPlatformData, projectData: IProjectData, prepareData: IPrepareData): Promise { + return new Promise(async (resolve, reject) => { + if (this.webpackProcesses[platformData.platformNameLowerCase]) { + resolve(); + return; + } + + let isFirstWebpackWatchCompilation = true; + prepareData.watch = true; + const childProcess = await this.startWebpackProcess(platformData, projectData, prepareData); + + 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, message.webpackRuntimeFiles, message.entryPointFiles); + + const files = result.emittedFiles + .map((file: string) => path.join(platformData.appDestinationDirectoryPath, "app", file)); + const fallbackFiles = result.fallbackFiles + .map((file: string) => path.join(platformData.appDestinationDirectoryPath, "app", file)); + + const data = { + files, + hmrData: { + hash: result.hash, + 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, prepareData: IPrepareData): Promise { + return new Promise(async (resolve, reject) => { + if (this.webpackProcesses[platformData.platformNameLowerCase]) { + resolve(); + return; + } + + const childProcess = await this.startWebpackProcess(platformData, projectData, prepareData); + 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, prepareData: IPrepareData): Promise { + const envData = this.buildEnvData(platformData.platformNameLowerCase, projectData, prepareData); + const envParams = this.buildEnvCommandLineParams(envData, platformData, prepareData); + + await this.$pluginsService.ensureAllDependenciesAreInstalled(projectData); + + const args = [ + path.join(projectData.projectDir, "node_modules", "webpack", "bin", "webpack.js"), + "--preserve-symlinks", + `--config=${path.join(projectData.projectDir, "webpack.config.js")}`, + ...envParams + ]; + + if (process.arch === "x64") { + args.push("--max_old_space_size=4096"); + } + + if (prepareData.watch) { + args.push("--watch"); + } + + const stdio = prepareData.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, projectData: IProjectData, prepareData: IPrepareData) { + const { env } = prepareData; + const envData = Object.assign({}, + env, + { [platform.toLowerCase()]: true } + ); + + const appPath = projectData.getAppDirectoryRelativePath(); + const appResourcesPath = projectData.getAppResourcesRelativeDirectoryPath(); + + Object.assign(envData, + appPath && { appPath }, + appResourcesPath && { appResourcesPath }, + ); + + envData.verbose = this.$logger.isVerbose(); + envData.production = prepareData.release; + if (prepareData.env && (prepareData.env.sourceMap === false || prepareData.env.sourceMap === 'false')) { + delete envData.sourceMap; + } else if (!prepareData.release) { + envData.sourceMap = true; + } + + return envData; + } + + private buildEnvCommandLineParams(envData: any, platformData: IPlatformData, prepareData: IPrepareData) { + const envFlagNames = Object.keys(envData); + const shouldSnapshot = prepareData.release && !this.$hostInfo.isWindows && this.$mobileHelper.isAndroidPlatform(platformData.normalizedPlatformName); + if (envData && envData.snapshot && !shouldSnapshot) { + this.$logger.warn("Stripping the snapshot flag. " + + "Bear in mind that snapshot is only available in release builds and " + + "is NOT available on Windows systems."); + envFlagNames.splice(envFlagNames.indexOf("snapshot"), 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[], webpackRuntimeFiles: string[], entryPointFiles: 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')); + if (webpackRuntimeFiles && webpackRuntimeFiles.length) { + result = result.filter(file => webpackRuntimeFiles.indexOf(file) === -1); + } + if (entryPointFiles && entryPointFiles.length) { + result = result.filter(file => entryPointFiles.indexOf(file) === -1); + } + 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..3a597b4ae2 --- /dev/null +++ b/lib/services/webpack/webpack.d.ts @@ -0,0 +1,148 @@ +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, prepareData: IPrepareData): Promise; + compileWithoutWatch(platformData: IPlatformData, projectData: IProjectData, prepareData: IPrepareData): Promise; + stopWebpackCompiler(platform: string): void; + } + + interface IWebpackEnvOptions { + sourceMap?: boolean; + uglify?: boolean; + production?: boolean; + } + + interface IProjectChangesService { + checkForChanges(platformData: IPlatformData, projectData: IProjectData, prepareData: IPrepareData): Promise; + getPrepareInfoFilePath(platformData: IPlatformData): string; + getPrepareInfo(platformData: IPlatformData): IPrepareInfo; + savePrepareInfo(platformData: IPlatformData, projectData: IProjectData, prepareData: IPrepareData): Promise; + setNativePlatformStatus(platformData: IPlatformData, projectData: IProjectData, addedPlatform: IAddedNativePlatform): void; + currentChanges: IProjectChangesInfo; + } + + interface IFilesChangeEventData { + platform: string; + files: string[]; + hmrData: IPlatformHmrData; + hasNativeChanges: boolean; + } + + 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(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. + * @returns {void} + */ + cleanProject?(projectRoot: string): 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-service.ts b/lib/services/workflow-service.ts deleted file mode 100644 index c9d36308a4..0000000000 --- a/lib/services/workflow-service.ts +++ /dev/null @@ -1,120 +0,0 @@ -import * as helpers from "../common/helpers"; -import * as path from "path"; -import * as semver from "semver"; -import { EOL } from "os"; -import { LoggerConfigData } from "../constants"; - -export class WorkflowService implements IWorkflowService { - private legacyWorkflowDeprecationMessage = `With the upcoming NativeScript 6.0 the Webpack workflow will become the only way of building apps. -More info about the reasons for this change and how to migrate your project can be found in the link below: -https://www.nativescript.org/blog/the-future-of-building-nativescript-apps`; - private webpackWorkflowConfirmMessage = `Do you want to switch your app to the Webpack workflow?`; - - constructor(private $bundleValidatorHelper: IBundleValidatorHelper, - private $fs: IFileSystem, - private $logger: ILogger, - private $packageManager: INodePackageManager, - private $projectDataService: IProjectDataService, - private $prompter: IPrompter, - private $options: IOptions - ) { - } - - public async handleLegacyWorkflow(options: IHandleLegacyWorkflowOptions): Promise { - const { projectDir, settings, skipWarnings, force } = options; - if (!settings.bundle || force) { - const projectData = this.$projectDataService.getProjectData(projectDir); - if (typeof (projectData.useLegacyWorkflow) !== "boolean" || force) { - const hasSwitched = await this.handleWebpackWorkflowSwitch(projectData, skipWarnings, force); - if (hasSwitched) { - this.$options.bundle = "webpack"; - this.$options.hmr = !settings.release; - if (typeof (settings.bundle) === "boolean") { - settings.bundle = true; - } else { - settings.bundle = this.$options.bundle; - } - settings.useHotModuleReload = this.$options.hmr; - } - } else if (!skipWarnings && projectData.useLegacyWorkflow === true) { - this.showLegacyWorkflowWarning(); - } else if (!skipWarnings && projectData.useLegacyWorkflow === false) { - this.showNoBundleWarning(); - } - } - } - - private async handleWebpackWorkflowSwitch(projectData: IProjectData, skipWarnings: boolean, force: boolean): Promise { - let hasSwitched = force; - if (force || helpers.isInteractive()) { - if (!force) { - this.$logger.info(); - this.$logger.printMarkdown(` -__Improve your project by switching to the Webpack workflow.__ - -\`${this.legacyWorkflowDeprecationMessage}\``); - hasSwitched = await this.$prompter.confirm(this.webpackWorkflowConfirmMessage, () => true); - } - - if (hasSwitched) { - this.$projectDataService.setUseLegacyWorkflow(projectData.projectDir, false); - await this.ensureWebpackPluginInstalled(projectData); - } else { - this.$projectDataService.setUseLegacyWorkflow(projectData.projectDir, true); - } - } else if (!skipWarnings) { - await this.showLegacyWorkflowWarning(); - } - - return hasSwitched; - } - - private showLegacyWorkflowWarning() { - const legacyWorkflowWarning = `You are using the Legacy Workflow.${EOL}${EOL}${this.legacyWorkflowDeprecationMessage}`; - - this.$logger.warn(legacyWorkflowWarning, { [LoggerConfigData.wrapMessageWithBorders]: true }); - } - - private showNoBundleWarning() { - const legacyWorkflowWarning = `You are using the '--no-bundle' flag which is switching to the Legacy Workflow.${EOL}${EOL}${this.legacyWorkflowDeprecationMessage}`; - - this.$logger.warn(legacyWorkflowWarning, { [LoggerConfigData.wrapMessageWithBorders]: true }); - } - - private async ensureWebpackPluginInstalled(projectData: IProjectData) { - const hmrOutOfBetaWebpackPluginVersion = "0.21.0"; - const webpackPluginName = "nativescript-dev-webpack"; - const webpackConfigFileName = "webpack.config.js"; - const validWebpackPluginTags = ["*", "latest", "next", "rc"]; - - let isInstalledVersionSupported = true; - const installedVersion = this.$bundleValidatorHelper.getBundlerDependencyVersion(projectData, webpackPluginName); - this.$logger.trace(`Updating to webpack workflow: Found ${webpackPluginName} v${installedVersion}`); - if (validWebpackPluginTags.indexOf(installedVersion) === -1) { - const isInstalledVersionValid = !!semver.valid(installedVersion) || !!semver.coerce(installedVersion); - isInstalledVersionSupported = - isInstalledVersionValid && semver.gte(semver.coerce(installedVersion), hmrOutOfBetaWebpackPluginVersion); - this.$logger.trace(`Updating to webpack workflow: Is installed version valid?: ${isInstalledVersionValid}`); - } - - this.$logger.trace(`Updating to webpack workflow: Is installed version supported?: ${isInstalledVersionSupported}`); - if (!isInstalledVersionSupported) { - const webpackConfigPath = path.join(projectData.projectDir, webpackConfigFileName); - if (this.$fs.exists(webpackConfigPath)) { - this.$fs.rename(webpackConfigPath, `${webpackConfigPath}.bak`); - this.$logger.warn(`The 'nativescript-dev-webpack' plugin was updated and your '${webpackConfigFileName}' was replaced. You can find your old '${webpackConfigPath}' in '${webpackConfigPath}.bak'.`); - } - - const installResult = await this.$packageManager.install(`${webpackPluginName}@latest`, projectData.projectDir, { - 'save-dev': true, - 'save-exact': true, - disableNpmInstall: false, - frameworkPath: null, - ignoreScripts: false, - }); - this.$logger.trace(`Updating to webpack workflow: The ${webpackPluginName} was updated to v${installResult.version}`); - } - } -} - -$injector.register("workflowService", WorkflowService); 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/services/xcproj-service.ts b/lib/services/xcproj-service.ts index 2344a89065..966f20880c 100644 --- a/lib/services/xcproj-service.ts +++ b/lib/services/xcproj-service.ts @@ -14,8 +14,8 @@ class XcprojService implements IXcprojService { private $xcodeSelectService: IXcodeSelectService) { } - public getXcodeprojPath(projectData: IProjectData, platformData: IPlatformData): string { - return path.join(platformData.projectRoot, projectData.projectName + IosProjectConstants.XcodeProjExtName); + public getXcodeprojPath(projectData: IProjectData, projectRoot: string): string { + return path.join(projectRoot, projectData.projectName + IosProjectConstants.XcodeProjExtName); } public async verifyXcproj(opts: IVerifyXcprojOptions): Promise { diff --git a/lib/tools/node-modules/node-modules-builder.ts b/lib/tools/node-modules/node-modules-builder.ts index 17f1c27317..a42d8df47d 100644 --- a/lib/tools/node-modules/node-modules-builder.ts +++ b/lib/tools/node-modules/node-modules-builder.ts @@ -1,47 +1,27 @@ -import { TnsModulesCopy, NpmPluginPrepare } from "./node-modules-dest-copy"; - export class NodeModulesBuilder implements INodeModulesBuilder { - constructor(private $fs: IFileSystem, - private $injector: IInjector, - private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder + constructor( + private $logger: ILogger, + private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder, + private $pluginsService: IPluginsService ) { } - public async prepareNodeModules(opts: INodeModulesBuilderData): Promise { - const productionDependencies = this.intialPrepareNodeModulesIfRequired(opts); - const npmPluginPrepare: NpmPluginPrepare = this.$injector.resolve(NpmPluginPrepare); - await npmPluginPrepare.preparePlugins(productionDependencies, opts.nodeModulesData.platform, opts.nodeModulesData.projectData, opts.nodeModulesData.projectFilesConfig); - } - - public async prepareJSNodeModules(opts: INodeModulesBuilderData): Promise { - const productionDependencies = this.intialPrepareNodeModulesIfRequired(opts); - const npmPluginPrepare: NpmPluginPrepare = this.$injector.resolve(NpmPluginPrepare); - await npmPluginPrepare.prepareJSPlugins(productionDependencies, opts.nodeModulesData.platform, opts.nodeModulesData.projectData, opts.nodeModulesData.projectFilesConfig); - } - - private intialPrepareNodeModulesIfRequired(opts: INodeModulesBuilderData): IDependencyData[] { - const { nodeModulesData } = opts; - const productionDependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies(nodeModulesData.projectData.projectDir); - - if (opts.copyNodeModules && !nodeModulesData.appFilesUpdaterOptions.bundle) { - this.initialPrepareNodeModules(opts, productionDependencies); + public async prepareNodeModules(platformData: IPlatformData, projectData: IProjectData): Promise { + const dependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir); + if (_.isEmpty(dependencies)) { + return; } - return productionDependencies; - } - - private initialPrepareNodeModules(opts: INodeModulesBuilderData, productionDependencies: IDependencyData[]): void { - const { nodeModulesData, release } = opts; + await platformData.platformProjectService.beforePrepareAllPlugins(projectData, dependencies); - if (!this.$fs.exists(nodeModulesData.absoluteOutputPath)) { - // Force copying if the destination doesn't exist. - nodeModulesData.lastModifiedTime = null; + 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); + } } - - const tnsModulesCopy: TnsModulesCopy = this.$injector.resolve(TnsModulesCopy, { - outputRoot: nodeModulesData.absoluteOutputPath - }); - - tnsModulesCopy.copyModules({ dependencies: productionDependencies, release }); } } 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 afcd8ccbf4..0000000000 --- a/lib/tools/node-modules/node-modules-dest-copy.ts +++ /dev/null @@ -1,192 +0,0 @@ -import * as path from "path"; -import * as shelljs from "shelljs"; -import * as constants from "../../constants"; -import * as minimatch from "minimatch"; - -export interface ILocalDependencyData extends IDependencyData { - directory: string; -} - -export class TnsModulesCopy { - constructor( - private outputRoot: string, - private $fs: IFileSystem, - private $pluginsService: IPluginsService - ) { - } - - public copyModules(opts: { dependencies: IDependencyData[], release: boolean }): void { - const filePatternsToDelete = opts.release ? "**/*.ts" : "**/*.d.ts"; - for (const entry in opts.dependencies) { - const dependency = opts.dependencies[entry]; - - this.copyDependencyDir(dependency, filePatternsToDelete); - } - } - - private copyDependencyDir(dependency: IDependencyData, filePatternsToDelete: string): void { - if (dependency.depth === 0) { - const targetPackageDir = path.join(this.outputRoot, dependency.name); - - shelljs.mkdir("-p", targetPackageDir); - - const isScoped = dependency.name.indexOf("@") === 0; - const destinationPath = isScoped ? path.join(this.outputRoot, dependency.name.substring(0, dependency.name.indexOf("/"))) : this.outputRoot; - shelljs.cp("-RfL", dependency.directory, destinationPath); - - // remove platform-specific files (processed separately by plugin services) - shelljs.rm("-rf", path.join(targetPackageDir, "platforms")); - - this.removeNonProductionDependencies(dependency, targetPackageDir); - this.removeDependenciesPlatformsDirs(targetPackageDir); - const allFiles = this.$fs.enumerateFilesInDirectorySync(targetPackageDir); - allFiles.filter(file => minimatch(file, filePatternsToDelete, { nocase: true })).map(file => this.$fs.deleteFile(file)); - } - } - - private removeDependenciesPlatformsDirs(dependencyDir: string): void { - const dependenciesFolder = path.join(dependencyDir, constants.NODE_MODULES_FOLDER_NAME); - - if (this.$fs.exists(dependenciesFolder)) { - const dependencies = this.getDependencies(dependenciesFolder); - - dependencies - .forEach(d => { - const pathToDependency = path.join(dependenciesFolder, d); - const pathToPackageJson = path.join(pathToDependency, constants.PACKAGE_JSON_FILE_NAME); - - if (this.$pluginsService.isNativeScriptPlugin(pathToPackageJson)) { - this.$fs.deleteDirectory(path.join(pathToDependency, constants.PLATFORMS_DIR_NAME)); - } - - this.removeDependenciesPlatformsDirs(pathToDependency); - }); - } - } - - private removeNonProductionDependencies(dependency: IDependencyData, targetPackageDir: string): void { - const packageJsonFilePath = path.join(dependency.directory, constants.PACKAGE_JSON_FILE_NAME); - if (!this.$fs.exists(packageJsonFilePath)) { - return; - } - - const packageJsonContent = this.$fs.readJson(packageJsonFilePath); - const productionDependencies = packageJsonContent.dependencies; - - const dependenciesFolder = path.join(targetPackageDir, constants.NODE_MODULES_FOLDER_NAME); - if (this.$fs.exists(dependenciesFolder)) { - const dependencies = this.getDependencies(dependenciesFolder); - - dependencies.filter(dir => !productionDependencies || !productionDependencies.hasOwnProperty(dir)) - .forEach(dir => shelljs.rm("-rf", path.join(dependenciesFolder, dir))); - } - } - - private getDependencies(dependenciesFolder: string): string[] { - const dependencies = _.flatten(this.$fs.readDirectory(dependenciesFolder) - .map(dir => { - if (_.startsWith(dir, "@")) { - const pathToDir = path.join(dependenciesFolder, dir); - const contents = this.$fs.readDirectory(pathToDir); - return _.map(contents, subDir => `${dir}/${subDir}`); - } - - return dir; - })); - - return dependencies; - } -} - -export class NpmPluginPrepare { - constructor( - private $fs: IFileSystem, - private $pluginsService: IPluginsService, - private $platformsData: IPlatformsData, - private $logger: ILogger - ) { - } - - protected async afterPrepare(dependencies: IDependencyData[], platform: string, projectData: IProjectData): Promise { - const prepareData: IDictionary = {}; - _.each(dependencies, d => { - prepareData[d.name] = true; - }); - this.$fs.createDirectory(this.preparedPlatformsDir(platform, projectData)); - this.$fs.writeJson(this.preparedPlatformsFile(platform, projectData), prepareData, " ", "utf8"); - } - - private preparedPlatformsDir(platform: string, projectData: IProjectData): string { - const platformRoot = this.$platformsData.getPlatformData(platform, projectData).projectRoot; - if (/android/i.test(platform)) { - return path.join(platformRoot, "build", "intermediates"); - } else if (/ios/i.test(platform)) { - return path.join(platformRoot, "build"); - } else { - throw new Error("Invalid platform: " + platform); - } - } - - private preparedPlatformsFile(platform: string, projectData: IProjectData): string { - return path.join(this.preparedPlatformsDir(platform, projectData), "prepared-platforms.json"); - } - - protected getPreviouslyPreparedDependencies(platform: string, projectData: IProjectData): IDictionary { - if (!this.$fs.exists(this.preparedPlatformsFile(platform, projectData))) { - return {}; - } - return this.$fs.readJson(this.preparedPlatformsFile(platform, projectData), "utf8"); - } - - private allPrepared(dependencies: IDependencyData[], platform: string, projectData: IProjectData): boolean { - let result = true; - const previouslyPrepared = this.getPreviouslyPreparedDependencies(platform, projectData); - _.each(dependencies, d => { - if (!previouslyPrepared[d.name]) { - result = false; - } - }); - return result; - } - - public async preparePlugins(dependencies: IDependencyData[], platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): 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); - } - } - - await this.afterPrepare(dependencies, platform, projectData); - } - - public async prepareJSPlugins(dependencies: IDependencyData[], platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise { - if (_.isEmpty(dependencies) || this.allPrepared(dependencies, platform, projectData)) { - return; - } - - for (const dependencyKey in dependencies) { - const dependency = dependencies[dependencyKey]; - const isPlugin = !!dependency.nativescript; - if (isPlugin) { - platform = platform.toLowerCase(); - const pluginData = this.$pluginsService.convertToPluginData(dependency, projectData.projectDir); - const platformData = this.$platformsData.getPlatformData(platform, projectData); - const appFolderExists = this.$fs.exists(path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME)); - if (appFolderExists) { - this.$pluginsService.preparePluginScripts(pluginData, platform, projectData, projectFilesConfig); - // Show message - this.$logger.info(`Successfully prepared plugin ${pluginData.name} for ${platform}.`); - } - } - } - } -} diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index d47e34730a..3ed51824b8 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2547,7 +2547,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -2565,11 +2566,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2582,15 +2585,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -2693,7 +2699,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -2703,6 +2710,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2715,17 +2723,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2742,6 +2753,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2814,7 +2826,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2824,6 +2837,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2899,7 +2913,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -2929,6 +2944,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2946,6 +2962,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2984,11 +3001,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, 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..4da557ea4c --- /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.info = (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/services/debug-service.ts b/test/controllers/debug-controller.ts similarity index 74% rename from test/services/debug-service.ts rename to test/controllers/debug-controller.ts index f839043e28..a0b6a64b3f 100644 --- a/test/services/debug-service.ts +++ b/test/controllers/debug-controller.ts @@ -1,10 +1,18 @@ -import { DebugService } from "../../lib/services/debug-service"; import { Yok } from "../../lib/common/yok"; import * as stubs from "../stubs"; import { assert } from "chai"; import { EventEmitter } from "events"; import * as constants from "../../lib/common/constants"; import { CONNECTION_ERROR_EVENT_NAME, DebugCommandErrors, TrackActionNames, DebugTools } from "../../lib/constants"; +import { DebugController } from "../../lib/controllers/debug-controller"; +import { BuildDataService } from "../../lib/services/build-data-service"; +import { DebugDataService } from "../../lib/services/debug-data-service"; +import { LiveSyncServiceResolver } from "../../lib/resolvers/livesync-service-resolver"; +import { PrepareDataService } from "../../lib/services/prepare-data-service"; +import { ProjectDataService } from "../../lib/services/project-data-service"; +import { StaticConfig } from "../../lib/config"; +import { DevicePlatformsConstants } from "../../lib/common/mobile/device-platforms-constants"; +import { LiveSyncProcessDataService } from "../../lib/services/livesync-process-data-service"; const fakeChromeDebugPort = 123; const fakeChromeDebugUrl = `fakeChromeDebugUrl?experiments=true&ws=localhost:${fakeChromeDebugPort}`; @@ -56,7 +64,7 @@ const getDefaultTestData = (platform?: string): IDebugTestData => ({ } }); -describe("debugService", () => { +describe("debugController", () => { const getTestInjectorForTestConfiguration = (testData: IDebugTestData): IInjector => { const testInjector = new Yok(); testInjector.register("devicesService", { @@ -97,24 +105,45 @@ describe("debugService", () => { trackEventActionInGoogleAnalytics: (data: IEventActionData) => Promise.resolve() }); + testInjector.register("buildDataService", BuildDataService); + testInjector.register("buildController", {}); + + testInjector.register("debugDataService", DebugDataService); + testInjector.register("deviceInstallAppService", {}); + testInjector.register("hmrStatusService", {}); + testInjector.register("hooksService", {}); + testInjector.register("liveSyncServiceResolver", LiveSyncServiceResolver); + testInjector.register("platformsDataService", {}); + testInjector.register("pluginsService", {}); + testInjector.register("prepareController", {}); + testInjector.register("prepareDataService", PrepareDataService); + testInjector.register("prepareNativePlatformService", {}); + testInjector.register("projectDataService", ProjectDataService); + testInjector.register("fs", {}); + testInjector.register("staticConfig", StaticConfig); + testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); + testInjector.register("androidResourcesMigrationService", {}); + testInjector.register("liveSyncProcessDataService", LiveSyncProcessDataService); + return testInjector; }; describe("debug", () => { - const getDebugData = (deviceIdentifier?: string): IDebugData => ({ - deviceIdentifier: deviceIdentifier || defaultDeviceIdentifier, + const getDebugData = (debugOptions?: IDebugOptions): IDebugData => ({ + deviceIdentifier: defaultDeviceIdentifier, applicationIdentifier: "org.nativescript.app1", projectDir: "/Users/user/app1", - projectName: "app1" + projectName: "app1", + debugOptions: debugOptions || {} }); describe("rejects the result promise when", () => { const assertIsRejected = async (testData: IDebugTestData, expectedError: string, userSpecifiedOptions?: IDebugOptions): Promise => { const testInjector = getTestInjectorForTestConfiguration(testData); - const debugService = testInjector.resolve(DebugService); + const debugController = testInjector.resolve(DebugController); const debugData = getDebugData(); - await assert.isRejected(debugService.debug(debugData, userSpecifiedOptions), expectedError); + await assert.isRejected(debugController.startDebug(debugData, userSpecifiedOptions), expectedError); }; it("there's no attached device as the specified identifier", async () => { @@ -138,14 +167,6 @@ describe("debugService", () => { await assertIsRejected(testData, "is not installed on device with identifier"); }); - it("the OS is neither Windows or macOS and device is iOS", async () => { - const testData = getDefaultTestData(); - testData.deviceInformation.deviceInfo.platform = "iOS"; - testData.hostInfo.isDarwin = testData.hostInfo.isWindows = false; - - await assertIsRejected(testData, "Debugging on iOS devices is not supported for"); - }); - it("device is neither iOS or Android", async () => { const testData = getDefaultTestData(); testData.deviceInformation.deviceInfo.platform = "WP8"; @@ -153,14 +174,6 @@ describe("debugService", () => { await assertIsRejected(testData, DebugCommandErrors.UNSUPPORTED_DEVICE_OS_FOR_DEBUGGING); }); - it("when trying to debug on iOS Simulator on macOS, debug-brk is passed, but pathToAppPackage is not", async () => { - const testData = getDefaultTestData(); - testData.deviceInformation.deviceInfo.platform = "iOS"; - testData.deviceInformation.isEmulator = true; - - await assertIsRejected(testData, "To debug on iOS simulator you need to provide path to the app package.", { debugBrk: true }); - }); - const assertIsRejectedWhenPlatformDebugServiceFails = async (platform: string): Promise => { const testData = getDefaultTestData(); testData.deviceInformation.deviceInfo.platform = platform; @@ -172,10 +185,10 @@ describe("debugService", () => { throw new Error(expectedErrorMessage); }; - const debugService = testInjector.resolve(DebugService); + const debugController = testInjector.resolve(DebugController); const debugData = getDebugData(); - await assert.isRejected(debugService.debug(debugData, null), expectedErrorMessage); + await assert.isRejected(debugController.startDebug(debugData, null), expectedErrorMessage); }; it("androidDeviceDebugService's debug method fails", async () => { @@ -194,14 +207,14 @@ describe("debugService", () => { testData.deviceInformation.deviceInfo.platform = platform; const testInjector = getTestInjectorForTestConfiguration(testData); - const debugService = testInjector.resolve(DebugService); + const debugController = testInjector.resolve(DebugController); let dataRaisedForConnectionError: any = null; - debugService.on(CONNECTION_ERROR_EVENT_NAME, (data: any) => { + debugController.on(CONNECTION_ERROR_EVENT_NAME, (data: any) => { dataRaisedForConnectionError = data; }); const debugData = getDebugData(); - await assert.isFulfilled(debugService.debug(debugData, null)); + await assert.isFulfilled(debugController.startDebug(debugData, null)); const expectedErrorData = { deviceIdentifier: "deviceId", message: "my message", code: 2048 }; const platformDebugService = testInjector.resolve(`${platform}DeviceDebugService`); @@ -218,10 +231,10 @@ describe("debugService", () => { testData.deviceInformation.deviceInfo.platform = platform; const testInjector = getTestInjectorForTestConfiguration(testData); - const debugService = testInjector.resolve(DebugService); + const debugController = testInjector.resolve(DebugController); const debugData = getDebugData(); - const debugInfo = await debugService.debug(debugData, null); + const debugInfo = await debugController.startDebug(debugData, null); assert.deepEqual(debugInfo, { url: fakeChromeDebugUrl, @@ -261,9 +274,9 @@ describe("debugService", () => { dataTrackedToGA = data; }; - const debugService = testInjector.resolve(DebugService); - const debugData = getDebugData(); - await debugService.debug(debugData, testCase.debugOptions); + const debugController = testInjector.resolve(DebugController); + const debugData = getDebugData(testCase.debugOptions); + await debugController.startDebug(debugData); const devicesService = testInjector.resolve("devicesService"); const device = devicesService.getDeviceByIdentifier(testData.deviceInformation.deviceInfo.identifier); diff --git a/test/controllers/prepare-controller.ts b/test/controllers/prepare-controller.ts new file mode 100644 index 0000000000..c730a3f6d5 --- /dev/null +++ b/test/controllers/prepare-controller.ts @@ -0,0 +1,125 @@ +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); + + injector.register("nodeModulesDependenciesBuilder", { + getProductionDependencies: () => ([]) + }); + + 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..9dc2e674da --- /dev/null +++ b/test/controllers/run-controller.ts @@ -0,0 +1,264 @@ +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 { 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"; +import { LiveSyncProcessDataService } from "../../lib/services/livesync-process-data-service"; + +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, buildData: {} }; +const androidDevice = { deviceInfo: { identifier: "myAndroidDevice", platform: "android" } }; +const androidDeviceDescriptor = { identifier: "myAndroidDevice", buildAction: async () => buildOutputPath, buildData: {} }; + +const map: IDictionary<{ device: Mobile.IDevice, descriptor: ILiveSyncDeviceDescriptor }> = { + 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("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("prepareDataService", PrepareDataService); + injector.register("buildDataService", BuildDataService); + injector.register("analyticsService", ({})); + injector.register("debugController", {}); + injector.register("liveSyncProcessDataService", LiveSyncProcessDataService); + + const devicesService = injector.resolve("devicesService"); + devicesService.getDevicesForPlatform = () => [{ identifier: "myTestDeviceId1" }]; + devicesService.getPlatformsFromDeviceDescriptors = (devices: ILiveSyncDeviceDescriptor[]) => devices.map(d => map[d.identifier].device.deviceInfo.platform); + devicesService.on = () => ({}); + + return injector; +} + +describe("RunController", () => { + let injector: IInjector = null; + let runController: RunController = null; + + beforeEach(() => { + isAttachToHmrStatusCalled = false; + prepareData = null; + + injector = createTestInjector(); + runController = injector.resolve("runController"); + }); + + describe("runOnDevices", () => { + describe("no watch", () => { + it("shouldn't start the watcher when skipWatcher flag is provided", async () => { + mockDevicesService(injector, [iOSDevice]); + + await runController.run({ + 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({ + 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({ + 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({ + 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({ + 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 () => { + const liveSyncProcessDataService = injector.resolve("liveSyncProcessDataService"); + (liveSyncProcessDataService).persistData(projectDir, testCase.currentDeviceIdentifiers.map(identifier => ({ identifier })), ["ios"]); + + const emittedDeviceIdentifiersForLiveSyncStoppedEvent: string[] = []; + + runController.on(RunOnDeviceEvents.runOnDeviceStopped, (data: any) => { + assert.equal(data.projectDir, projectDir); + emittedDeviceIdentifiersForLiveSyncStoppedEvent.push(data.deviceIdentifier); + }); + + await runController.stop({ projectDir, deviceIdentifiers: testCase.deviceIdentifiersToBeStopped }); + + assert.deepEqual(emittedDeviceIdentifiersForLiveSyncStoppedEvent, testCase.expectedDeviceIdentifiers); + }); + } + }); +}); diff --git a/test/debug.ts b/test/debug.ts deleted file mode 100644 index e41315dacc..0000000000 --- a/test/debug.ts +++ /dev/null @@ -1,116 +0,0 @@ -import * as stubs from "./stubs"; -import * as yok from "../lib/common/yok"; -import { DebugAndroidCommand } from "../lib/commands/debug"; -import { assert } from "chai"; -import { BundleValidatorHelper } from "../lib/helpers/bundle-validator-helper"; -import { Configuration, StaticConfig } from "../lib/config"; -import { Options } from "../lib/options"; -import { DevicePlatformsConstants } from "../lib/common/mobile/device-platforms-constants"; -import { FileSystem } from "../lib/common/file-system"; -import { AndroidProjectService } from "../lib/services/android-project-service"; -import { AndroidDebugBridge } from "../lib/common/mobile/android/android-debug-bridge"; -import { AndroidDebugBridgeResultHandler } from "../lib/common/mobile/android/android-debug-bridge-result-handler"; -import { SettingsService } from "../lib/common/test/unit-tests/stubs"; - -function createTestInjector(): IInjector { - const testInjector: IInjector = new yok.Yok(); - - testInjector.register("workflowService", stubs.WorkflowServiceStub); - testInjector.register("debug|android", DebugAndroidCommand); - testInjector.register("config", Configuration); - testInjector.register("staticConfig", StaticConfig); - testInjector.register("logger", stubs.LoggerStub); - testInjector.register("options", Options); - testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); - testInjector.register('childProcess', stubs.ChildProcessStub); - testInjector.register('fs', FileSystem); - testInjector.register('errors', stubs.ErrorsStub); - testInjector.register('hostInfo', {}); - testInjector.register("androidBundleValidatorHelper", stubs.AndroidBundleValidatorHelper); - testInjector.register("bundleValidatorHelper", BundleValidatorHelper); - testInjector.register("analyticsService", { - trackException: async (): Promise => undefined, - checkConsent: async (): Promise => undefined, - trackFeature: async (): Promise => undefined - }); - testInjector.register('devicesService', { - initialize: async () => { /* Intentionally left blank */ }, - getDeviceInstances: (): any[] => { return []; }, - execute: async (): Promise => ({}) - }); - testInjector.register("liveSyncService", stubs.LiveSyncServiceStub); - testInjector.register("androidProjectService", AndroidProjectService); - testInjector.register("androidToolsInfo", stubs.AndroidToolsInfoStub); - testInjector.register("hostInfo", {}); - testInjector.register("projectData", { platformsDir: "test", initializeProjectData: () => { /* empty */ } }); - testInjector.register("projectDataService", {}); - testInjector.register("sysInfo", {}); - testInjector.register("mobileHelper", {}); - testInjector.register("pluginVariablesService", {}); - testInjector.register("projectTemplatesService", {}); - testInjector.register("debugService", {}); - testInjector.register("xmlValidator", {}); - testInjector.register("packageManager", {}); - testInjector.register("debugDataService", { - createDebugData: () => ({}) - }); - testInjector.register("androidEmulatorServices", {}); - testInjector.register("adb", AndroidDebugBridge); - testInjector.register("androidDebugBridgeResultHandler", AndroidDebugBridgeResultHandler); - testInjector.register("platformService", stubs.PlatformServiceStub); - testInjector.register("platformsData", { - availablePlatforms: { - Android: "Android", - iOS: "iOS" - } - }); - - testInjector.register("prompter", {}); - testInjector.registerCommand("debug|android", DebugAndroidCommand); - testInjector.register("liveSyncCommandHelper", { - executeLiveSyncOperation: async (): Promise => { - return null; - } - }); - testInjector.register("settingsService", SettingsService); - testInjector.register("androidPluginBuildService", stubs.AndroidPluginBuildServiceStub); - testInjector.register("platformEnvironmentRequirements", {}); - testInjector.register("androidResourcesMigrationService", stubs.AndroidResourcesMigrationServiceStub); - testInjector.register("filesHashService", {}); - - return testInjector; -} - -describe("debug command tests", () => { - describe("Debugger tests", () => { - let testInjector: IInjector; - - beforeEach(() => { - testInjector = createTestInjector(); - }); - - it("Ensures that beforePrepareAllPlugins will call gradle with clean option when *NOT* livesyncing", async () => { - const platformData = testInjector.resolve("platformsData"); - platformData.frameworkPackageName = "tns-android"; - - // only test that 'clean' is performed on android <=3.2. See https://github.com/NativeScript/nativescript-cli/pull/3032 - const projectDataService: IProjectDataService = testInjector.resolve("projectDataService"); - projectDataService.getNSValue = (projectDir: string, propertyName: string) => { - return { version: "3.2.0" }; - }; - - const childProcess: stubs.ChildProcessStub = testInjector.resolve("childProcess"); - const androidProjectService: IPlatformProjectService = testInjector.resolve("androidProjectService"); - androidProjectService.getPlatformData = (_projectData: IProjectData): IPlatformData => { - return platformData; - }; - const projectData: IProjectData = testInjector.resolve("projectData"); - const spawnFromEventCount = childProcess.spawnFromEventCount; - await androidProjectService.beforePrepareAllPlugins(projectData); - assert.isTrue(childProcess.lastCommand.indexOf("gradle") !== -1); - assert.isTrue(childProcess.lastCommandArgs[0] === "clean"); - assert.isTrue(spawnFromEventCount === 0); - assert.isTrue(spawnFromEventCount + 1 === childProcess.spawnFromEventCount); - }); - }); -}); 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 0fad626fd0..edd04e0fce 100644 --- a/test/ios-project-service.ts +++ b/test/ios-project-service.ts @@ -1,4 +1,4 @@ -import { join, resolve, dirname, basename, extname } from "path"; +import { join, dirname, basename, extname } from "path"; import { EOL } from "os"; import * as ChildProcessLib from "../lib/common/child-process"; import * as ConfigLib from "../lib/config"; @@ -21,8 +21,6 @@ import { LoggingLevels } from "../lib/common/mobile/logging-levels"; import { DeviceDiscovery } from "../lib/common/mobile/mobile-core/device-discovery"; import { IOSDeviceDiscovery } from "../lib/common/mobile/mobile-core/ios-device-discovery"; import { AndroidDeviceDiscovery } from "../lib/common/mobile/mobile-core/android-device-discovery"; -import { PluginVariablesService } from "../lib/services/plugin-variables-service"; -import { PluginVariablesHelper } from "../lib/common/plugin-variables-helper"; import { Utils } from "../lib/common/utils"; import { CocoaPodsService } from "../lib/services/cocoapods-service"; import { PackageManager } from "../lib/package-manager"; @@ -30,13 +28,17 @@ 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"; import { xcode } from "../lib/node/xcode"; import temp = require("temp"); import { CocoaPodsPlatformManager } from "../lib/services/cocoapods-platform-manager"; +import { XcodebuildService } from "../lib/services/ios/xcodebuild-service"; +import { XcodebuildCommandService } from "../lib/services/ios/xcodebuild-command-service"; +import { XcodebuildArgsService } from "../lib/services/ios/xcodebuild-args-service"; +import { ExportOptionsPlistService } from "../lib/services/ios/export-options-plist-service"; +import { IOSSigningService } from "../lib/services/ios/ios-signing-service"; temp.track(); class IOSSimulatorDiscoveryMock extends DeviceDiscovery { @@ -107,14 +109,12 @@ function createTestInjector(projectPath: string, projectName: string, xCode?: IX shouldUseXcproj: false }; }, - getXcodeprojPath: (projData: IProjectData, platformData: IPlatformData) => { - return join(platformData.projectRoot, projData.projectName + ".xcodeproj"); + getXcodeprojPath: (projData: IProjectData, projectRoot: string) => { + return join(projectRoot, projData.projectName + ".xcodeproj"); }, checkIfXcodeprojIsRequired: () => ({}) }); testInjector.register("iosDeviceOperations", {}); - testInjector.register("pluginVariablesService", PluginVariablesService); - testInjector.register("pluginVariablesHelper", PluginVariablesHelper); testInjector.register("pluginsService", { getAllInstalledPlugins: (): string[] => [] }); @@ -159,6 +159,13 @@ function createTestInjector(projectPath: string, projectName: string, xCode?: IX removeExtensions: () => { /* */ }, addExtensionsFromPath: () => Promise.resolve() }); + testInjector.register("timers", {}); + testInjector.register("iOSSigningService", IOSSigningService); + testInjector.register("xcodebuildService", XcodebuildService); + testInjector.register("xcodebuildCommandService", XcodebuildCommandService); + testInjector.register("xcodebuildArgsService", XcodebuildArgsService); + testInjector.register("exportOptionsPlistService", ExportOptionsPlistService); + testInjector.register("iOSWatchAppService", { removeWatchApp: () => { /* */ }, addWatchAppFromPath: () => Promise.resolve() @@ -182,178 +189,6 @@ function createPackageJson(testInjector: IInjector, projectPath: string, project testInjector.resolve("fs").writeJson(join(projectPath, "package.json"), packageJsonData); } -function expectOption(args: string[], option: string, value: string, message?: string): void { - const index = args.indexOf(option); - assert.ok(index >= 0, "Expected " + option + " to be set."); - assert.ok(args.length > index + 1, "Expected " + option + " to have value"); - assert.equal(args[index + 1], value, message); -} - -function readOption(args: string[], option: string): string { - const index = args.indexOf(option); - assert.ok(index >= 0, "Expected " + option + " to be set."); - assert.ok(args.length > index + 1, "Expected " + option + " to have value"); - return args[index + 1]; -} - -describe("iOSProjectService", () => { - describe("archive", () => { - async function setupArchive(options?: { archivePath?: string }): Promise<{ run: () => Promise, assert: () => void }> { - const hasCustomArchivePath = options && options.archivePath; - - const projectName = "projectDirectory"; - const projectPath = temp.mkdirSync(projectName); - - const testInjector = createTestInjector(projectPath, projectName); - const iOSProjectService = testInjector.resolve("iOSProjectService"); - const projectData: IProjectData = testInjector.resolve("projectData"); - - const childProcess = testInjector.resolve("childProcess"); - let xcodebuildExeced = false; - - let archivePath: string; - - childProcess.spawnFromEvent = (cmd: string, args: string[]) => { - assert.equal(cmd, "xcodebuild", "Expected iOSProjectService.archive to call xcodebuild.archive"); - xcodebuildExeced = true; - - if (hasCustomArchivePath) { - archivePath = resolve(options.archivePath); - } else { - archivePath = join(projectPath, "platforms", "ios", "build", "Release-iphoneos", projectName + ".xcarchive"); - } - - assert.ok(args.indexOf("archive") >= 0, "Expected xcodebuild to be executed with archive param."); - - expectOption(args, "-archivePath", archivePath, hasCustomArchivePath ? "Wrong path passed to xcarchive" : "exports xcodearchive to platforms/ios/build/archive."); - expectOption(args, "-project", join(projectPath, "platforms", "ios", projectName + ".xcodeproj"), "Path to Xcode project is wrong."); - expectOption(args, "-scheme", projectName, "The provided scheme is wrong."); - - return Promise.resolve(); - }; - - let resultArchivePath: string; - - return { - run: async (): Promise => { - if (hasCustomArchivePath) { - resultArchivePath = await iOSProjectService.archive(projectData, null, { archivePath: options.archivePath }); - } else { - resultArchivePath = await iOSProjectService.archive(projectData, null); - } - }, - assert: () => { - assert.ok(xcodebuildExeced, "Expected xcodebuild archive to be executed"); - assert.equal(resultArchivePath, archivePath, "iOSProjectService.archive expected to return the path to the archive"); - } - }; - } - - if (require("os").platform() !== "darwin") { - console.log("Skipping iOS archive tests. They can work only on macOS"); - } else { - it("by default exports xcodearchive to platforms/ios/build/archive/.xcarchive", async () => { - const setup = await setupArchive(); - await setup.run(); - setup.assert(); - }); - it("can pass archivePath to xcodebuild -archivePath", async () => { - const setup = await setupArchive({ archivePath: "myarchive.xcarchive" }); - await setup.run(); - setup.assert(); - }); - } - }); - - describe("exportArchive", () => { - const noTeamPlist = ` - - - - method - app-store - uploadBitcode - - compileBitcode - - uploadSymbols - - -`; - - const myTeamPlist = ` - - - - teamID - MyTeam - method - app-store - uploadBitcode - - compileBitcode - - uploadSymbols - - -`; - - async function testExportArchive(options: { teamID?: string }, expectedPlistContent: string): Promise { - const projectName = "projectDirectory"; - const projectPath = temp.mkdirSync(projectName); - - const testInjector = createTestInjector(projectPath, projectName); - const iOSProjectService = testInjector.resolve("iOSProjectService"); - const projectData: IProjectData = testInjector.resolve("projectData"); - - const archivePath = join(projectPath, "platforms", "ios", "build", "archive", projectName + ".xcarchive"); - - const childProcess = testInjector.resolve("childProcess"); - const fs = testInjector.resolve("fs"); - - let xcodebuildExeced = false; - - childProcess.spawnFromEvent = (cmd: string, args: string[]) => { - assert.equal(cmd, "xcodebuild", "Expected xcodebuild to be called"); - xcodebuildExeced = true; - - assert.ok(args.indexOf("-exportArchive") >= 0, "Expected -exportArchive to be set on xcodebuild."); - - expectOption(args, "-archivePath", archivePath, "Expected the -archivePath to be passed to xcodebuild."); - expectOption(args, "-exportPath", join(projectPath, "platforms", "ios", "build", "archive"), "Expected the -archivePath to be passed to xcodebuild."); - const plist = readOption(args, "-exportOptionsPlist"); - - assert.ok(plist); - - const plistContent = fs.readText(plist); - // There may be better way to equal property lists - assert.equal(plistContent, expectedPlistContent, "Mismatch in exportOptionsPlist content"); - - return Promise.resolve(); - }; - - const resultIpa = await iOSProjectService.exportArchive(projectData, { archivePath, teamID: options.teamID }); - const expectedIpa = join(projectPath, "platforms", "ios", "build", "archive", projectName + ".ipa"); - - assert.equal(resultIpa, expectedIpa, "Expected IPA at the specified location"); - - assert.ok(xcodebuildExeced, "Expected xcodebuild to be executed"); - } - - if (require("os").platform() !== "darwin") { - console.log("Skipping iOS export archive tests. They can work only on macOS"); - } else { - it("calls xcodebuild -exportArchive to produce .IPA", async () => { - await testExportArchive({}, noTeamPlist); - }); - - it("passes the --team-id option down the xcodebuild -exportArchive throug the -exportOptionsPlist", async () => { - await testExportArchive({ teamID: "MyTeam" }, myTeamPlist); - }); - } - }); -}); - describe("Cocoapods support", () => { if (require("os").platform() !== "darwin") { console.log("Skipping Cocoapods tests. They cannot work on windows"); @@ -879,250 +714,6 @@ 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("specifying provision", () => { - describe("from Automatic to provision name", () => { - beforeEach(() => { - files[pbxproj] = ""; - pbxprojDomXcode.Xcode.open = function (path: string) { - return { - getSigning(x: string) { - return { style: "Automatic", teamID: "AutoTeam" }; - } - }; - }; - }); - it("fails with proper error if the provision can not be found", async () => { - try { - await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptDev2", teamId: undefined }); - } catch (e) { - assert.isTrue(e.toString().indexOf("Failed to find mobile provision with UUID or Name: NativeScriptDev2") >= 0); - } - }); - it("succeeds if the provision name is provided for development cert", async () => { - const stack: any = []; - pbxprojDomXcode.Xcode.open = function (path: string) { - assert.equal(path, pbxproj); - return { - getSigning() { - return { style: "Automatic", teamID: "AutoTeam" }; - }, - save() { - stack.push("save()"); - }, - setManualSigningStyle(targetName: string, manualSigning: any) { - stack.push({ targetName, manualSigning }); - }, - setManualSigningStyleByTargetProductType: () => ({}), - setManualSigningStyleByTargetProductTypesList: () => ({}), - setManualSigningStyleByTargetKey: () => ({}) - }; - }; - await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptDev", teamId: undefined }); - assert.deepEqual(stack, [{ targetName: projectDirName, manualSigning: { team: "TKID101", uuid: "12345", name: "NativeScriptDev", identity: "iPhone Developer" } }, "save()"]); - }); - it("succeds if the provision name is provided for distribution cert", async () => { - const stack: any = []; - pbxprojDomXcode.Xcode.open = function (path: string) { - assert.equal(path, pbxproj); - return { - getSigning() { - return { style: "Automatic", teamID: "AutoTeam" }; - }, - save() { - stack.push("save()"); - }, - setManualSigningStyle(targetName: string, manualSigning: any) { - stack.push({ targetName, manualSigning }); - }, - setManualSigningStyleByTargetProductType: () => ({}), - setManualSigningStyleByTargetProductTypesList: () => ({}), - setManualSigningStyleByTargetKey: () => ({}) - }; - }; - await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptDist", teamId: undefined }); - assert.deepEqual(stack, [{ targetName: projectDirName, manualSigning: { team: "TKID202", uuid: "6789", name: "NativeScriptDist", identity: "iPhone Distribution" } }, "save()"]); - }); - it("succeds if the provision name is provided for adhoc cert", async () => { - const stack: any = []; - pbxprojDomXcode.Xcode.open = function (path: string) { - assert.equal(path, pbxproj); - return { - getSigning() { - return { style: "Automatic", teamID: "AutoTeam" }; - }, - save() { - stack.push("save()"); - }, - setManualSigningStyle(targetName: string, manualSigning: any) { - stack.push({ targetName, manualSigning }); - }, - setManualSigningStyleByTargetProductType: () => ({}), - setManualSigningStyleByTargetProductTypesList: () => ({}), - setManualSigningStyleByTargetKey: () => ({}) - }; - }; - await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptAdHoc", teamId: undefined }); - assert.deepEqual(stack, [{ targetName: projectDirName, manualSigning: { team: "TKID303", uuid: "1010", name: "NativeScriptAdHoc", identity: "iPhone Distribution" } }, "save()"]); - }); - }); - }); -}); - describe("Merge Project XCConfig files", () => { if (require("os").platform() !== "darwin") { console.log("Skipping 'Merge Project XCConfig files' tests. They can work only on macOS"); @@ -1260,164 +851,6 @@ describe("Merge Project XCConfig files", () => { }); }); -describe("buildProject", () => { - let xcodeBuildCommandArgs: string[] = []; - - function setup(data: { frameworkVersion: string, deploymentTarget: string, devices?: Mobile.IDevice[] }): IInjector { - const projectPath = "myTestProjectPath"; - const projectName = "myTestProjectName"; - const testInjector = createTestInjector(projectPath, projectName); - - const childProcess = testInjector.resolve("childProcess"); - childProcess.spawnFromEvent = (command: string, args: string[]) => { - if (command === "xcodebuild" && args[0] !== "-exportArchive") { - xcodeBuildCommandArgs = args; - } - }; - - const projectDataService = testInjector.resolve("projectDataService"); - projectDataService.getNSValue = (projectDir: string, propertyName: string) => { - if (propertyName === "tns-ios") { - return { - name: "tns-ios", - version: data.frameworkVersion - }; - } - }; - - const projectData = testInjector.resolve("projectData"); - projectData.appResourcesDirectoryPath = join(projectPath, "app", "App_Resources"); - - const devicesService = testInjector.resolve("devicesService"); - devicesService.initialize = () => ({}); - devicesService.getDeviceInstances = () => data.devices || []; - - const xcconfigService = testInjector.resolve("xcconfigService"); - xcconfigService.readPropertyValue = (projectDir: string, propertyName: string) => { - if (propertyName === "IPHONEOS_DEPLOYMENT_TARGET") { - return data.deploymentTarget; - } - }; - - const pbxprojDomXcode = testInjector.resolve("pbxprojDomXcode"); - pbxprojDomXcode.Xcode = { - open: () => ({ - getSigning: () => ({}), - setAutomaticSigningStyle: () => ({}), - setAutomaticSigningStyleByTargetProductType: () => ({}), - setAutomaticSigningStyleByTargetProductTypesList: () => ({}), - setAutomaticSigningStyleByTargetKey: () => ({}), - save: () => ({}) - }) - }; - - const iOSProvisionService = testInjector.resolve("iOSProvisionService"); - iOSProvisionService.getDevelopmentTeams = () => ({}); - iOSProvisionService.getTeamIdsWithName = () => ({}); - - return testInjector; - } - - function executeTests(testCases: any[], data: { buildForDevice: boolean }) { - _.each(testCases, testCase => { - it(`${testCase.name}`, async () => { - const testInjector = setup({ frameworkVersion: testCase.frameworkVersion, deploymentTarget: testCase.deploymentTarget }); - const projectData: IProjectData = testInjector.resolve("projectData"); - - const iOSProjectService = testInjector.resolve("iOSProjectService"); - (iOSProjectService).getExportOptionsMethod = () => ({}); - await iOSProjectService.buildProject("myProjectRoot", projectData, { buildForDevice: data.buildForDevice }); - - const archsItem = xcodeBuildCommandArgs.find(item => item.startsWith("ARCHS=")); - if (testCase.expectedArchs) { - const archsValue = archsItem.split("=")[1]; - assert.deepEqual(archsValue, testCase.expectedArchs); - } else { - assert.deepEqual(undefined, archsItem); - } - }); - }); - } - - describe("for device", () => { - afterEach(() => { - xcodeBuildCommandArgs = []; - }); - - const testCases = [{ - name: "shouldn't exclude armv7 architecture when deployment target 10", - frameworkVersion: "5.0.0", - deploymentTarget: "10.0", - expectedArchs: "armv7 arm64" - }, { - name: "should exclude armv7 architecture when deployment target is 11", - frameworkVersion: "5.0.0", - deploymentTarget: "11.0", - expectedArchs: "arm64" - }, { - name: "shouldn't pass architecture to xcodebuild command when frameworkVersion is 5.1.0", - frameworkVersion: "5.1.0", - deploymentTarget: "11.0" - }, { - name: "should pass only 64bit architecture to xcodebuild command when frameworkVersion is 5.0.0 and deployment target is 11.0", - frameworkVersion: "5.0.0", - deploymentTarget: "11.0", - expectedArchs: "arm64" - }, { - name: "should pass both architectures to xcodebuild command when frameworkVersion is 5.0.0 and deployment target is 10.0", - frameworkVersion: "5.0.0", - deploymentTarget: "10.0", - expectedArchs: "armv7 arm64" - }, { - name: "should pass both architectures to xcodebuild command when frameworkVersion is 5.0.0 and no deployment target", - frameworkVersion: "5.0.0", - deploymentTarget: null, - expectedArchs: "armv7 arm64" - }]; - - executeTests(testCases, { buildForDevice: true }); - }); - - describe("for simulator", () => { - afterEach(() => { - xcodeBuildCommandArgs = []; - }); - - const testCases = [{ - name: "shouldn't exclude i386 architecture when deployment target is 10", - frameworkVersion: "5.0.0", - deploymentTarget: "10.0", - expectedArchs: "i386 x86_64" - }, { - name: "should exclude i386 architecture when deployment target is 11", - frameworkVersion: "5.0.0", - deploymentTarget: "11.0", - expectedArchs: "x86_64" - }, { - name: "shouldn't pass architecture to xcodebuild command when frameworkVersion is 5.1.0", - frameworkVersion: "5.1.0", - deploymentTarget: "11.0" - }, { - name: "should pass only 64bit architecture to xcodebuild command when frameworkVersion is 5.0.0 and deployment target is 11.0", - frameworkVersion: "5.0.0", - deploymentTarget: "11.0", - expectedArchs: "x86_64" - }, { - name: "should pass both architectures to xcodebuild command when frameworkVersion is 5.0.0 and deployment target is 10.0", - frameworkVersion: "5.0.0", - deploymentTarget: "10.0", - expectedArchs: "i386 x86_64" - }, { - name: "should pass both architectures to xcodebuild command when frameworkVersion is 5.0.0 and no deployment target", - frameworkVersion: "5.0.0", - deploymentTarget: null, - expectedArchs: "i386 x86_64" - }]; - - executeTests(testCases, { buildForDevice: false }); - }); -}); - describe("handleNativeDependenciesChange", () => { it("ensure the correct order of pod install and merging pod's xcconfig file", async () => { const executedCocoapodsMethods: string[] = []; diff --git a/test/nativescript-cli-lib.ts b/test/nativescript-cli-lib.ts index 33b08b031c..d51dc9cfa7 100644 --- a/test/nativescript-cli-lib.ts +++ b/test/nativescript-cli-lib.ts @@ -23,13 +23,14 @@ describe("nativescript-cli-lib", () => { "getIOSAssetsStructure", "getAndroidAssetsStructure" ], + buildController: ["build"], constants: ["CONFIG_NS_APP_RESOURCES_ENTRY", "CONFIG_NS_APP_ENTRY", "CONFIG_NS_FILE_NAME", "LoggerLevel", "LoggerAppenders"], - localBuildService: ["build"], deviceLogProvider: null, packageManager: ["install", "uninstall", "view", "search"], extensibilityService: ["loadExtensions", "loadExtension", "getInstalledExtensions", "installExtension", "uninstallExtension"], - liveSyncService: ["liveSync", "stopLiveSync", "enableDebugging", "disableDebugging", "attachDebugger"], - debugService: ["debug"], + runController: ["run", "stop"], + debugController: ["enableDebugging", "disableDebugging", "attachDebugger"], + previewAppController: ["startPreview", "stopPreview"], analyticsSettingsService: ["getClientId"], devicesService: [ "addDeviceDiscovery", diff --git a/test/options.ts b/test/options.ts index 444fbf444b..0e21876dd5 100644 --- a/test/options.ts +++ b/test/options.ts @@ -268,9 +268,9 @@ describe("options", () => { name: "no options are provided", args: [], data: [ - { useLegacyWorkflow: undefined, expectedHmr: false, expectedBundle: false }, + { useLegacyWorkflow: undefined, expectedHmr: true, expectedBundle: true }, { useLegacyWorkflow: false, expectedHmr: true, expectedBundle: true }, - { useLegacyWorkflow: true, expectedHmr: false, expectedBundle: false } + { useLegacyWorkflow: true, expectedHmr: true, expectedBundle: true } ] }, { @@ -286,36 +286,27 @@ describe("options", () => { name: " --no-hmr is provided", args: ["--no-hmr"], data: [ - { useLegacyWorkflow: undefined, expectedHmr: false, expectedBundle: false }, + { useLegacyWorkflow: undefined, expectedHmr: false, expectedBundle: true }, { useLegacyWorkflow: false, expectedHmr: false, expectedBundle: true }, - { useLegacyWorkflow: true, expectedHmr: false, expectedBundle: false } + { useLegacyWorkflow: true, expectedHmr: false, expectedBundle: true } ] }, { name: " --bundle is provided", args: ["--bundle"], data: [ - { useLegacyWorkflow: undefined, expectedHmr: false, expectedBundle: true }, + { useLegacyWorkflow: undefined, expectedHmr: true, expectedBundle: true }, { useLegacyWorkflow: false, expectedHmr: true, expectedBundle: true }, - { useLegacyWorkflow: true, expectedHmr: false, expectedBundle: true } - ] - }, - { - name: " --no-bundle is provided", - args: ["--no-bundle"], - data: [ - { useLegacyWorkflow: undefined, expectedHmr: false, expectedBundle: false }, - { useLegacyWorkflow: false, expectedHmr: false, expectedBundle: false }, - { useLegacyWorkflow: true, expectedHmr: false, expectedBundle: false } + { useLegacyWorkflow: true, expectedHmr: true, expectedBundle: true } ] }, { name: " --release is provided", args: ["--release"], data: [ - { useLegacyWorkflow: undefined, expectedHmr: false, expectedBundle: false }, + { useLegacyWorkflow: undefined, expectedHmr: false, expectedBundle: true }, { useLegacyWorkflow: false, expectedHmr: false, expectedBundle: true }, - { useLegacyWorkflow: true, expectedHmr: false, expectedBundle: false } + { useLegacyWorkflow: true, expectedHmr: false, expectedBundle: true } ] } ]; @@ -344,11 +335,6 @@ describe("options", () => { name: "--release --hmr", args: ["--release", "--hmr"], expectedError: "The options --release and --hmr cannot be used simultaneously." - }, - { - name: "--no-bundle --hmr", - args: ["--no-bundle", "--hmr"], - expectedError: "The options --no-bundle and --hmr cannot be used simultaneously." } ]; diff --git a/test/package-installation-manager.ts b/test/package-installation-manager.ts index 242324f26e..d4b303f4ad 100644 --- a/test/package-installation-manager.ts +++ b/test/package-installation-manager.ts @@ -14,12 +14,11 @@ import * as yok from "../lib/common/yok"; import ChildProcessLib = require("../lib/common/child-process"); import { SettingsService } from "../lib/common/test/unit-tests/stubs"; import { ProjectDataService } from "../lib/services/project-data-service"; -import { WorkflowServiceStub, ProjectDataStub } from "./stubs"; +import { ProjectDataStub } from "./stubs"; function createTestInjector(): IInjector { const testInjector = new yok.Yok(); - testInjector.register("workflowService", WorkflowServiceStub); testInjector.register("projectData", ProjectDataStub); testInjector.register("config", ConfigLib.Configuration); testInjector.register("logger", LoggerLib.Logger); diff --git a/test/platform-commands.ts b/test/platform-commands.ts index 9b593a1b73..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,13 +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 { WorkflowServiceStub } from "./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 { @@ -79,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(); } @@ -98,16 +99,16 @@ function createTestInjector() { const testInjector = new yok.Yok(); testInjector.register("injector", testInjector); - testInjector.register("workflowService", WorkflowServiceStub); 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', {}); @@ -138,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", { @@ -184,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"); }); @@ -478,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 5ae8e12f63..0000000000 --- a/test/platform-service.ts +++ /dev/null @@ -1,1210 +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 { INFO_PLIST_FILE_NAME, MANIFEST_FILE_NAME } from "../lib/constants"; -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("workflowService", stubs.WorkflowServiceStub); - 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; -} - -class CreatedTestData { - files: string[]; - - resources: { - ios: string[], - android: string[] - }; - - testDirData: { - tempFolder: string, - appFolderPath: string, - app1FolderPath: string, - appDestFolderPath: string, - appResourcesFolderPath: string - }; - - constructor() { - this.files = []; - this.resources = { - ios: [], - android: [] - }; - - this.testDirData = { - tempFolder: "", - appFolderPath: "", - app1FolderPath: "", - appDestFolderPath: "", - appResourcesFolderPath: "" - }; - } -} - -class DestinationFolderVerifier { - static verify(data: any, fs: IFileSystem) { - _.forOwn(data, (folder, folderRoot) => { - _.each(folder.filesWithContent || [], (file) => { - const filePath = path.join(folderRoot, file.name); - assert.isTrue(fs.exists(filePath), `Expected file ${filePath} to be present.`); - assert.equal(fs.readFile(filePath).toString(), file.content, `File content for ${filePath} doesn't match.`); - }); - - _.each(folder.missingFiles || [], (file) => { - assert.isFalse(fs.exists(path.join(folderRoot, file)), `Expected file ${file} to be missing.`); - }); - - _.each(folder.presentFiles || [], (file) => { - assert.isTrue(fs.exists(path.join(folderRoot, file)), `Expected file ${file} to be present.`); - }); - }); - } -} - -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)); - }); - }); - }); - - 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 }; - platformService.getCurrentPlatformVersion = () => "5.1.1"; - 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"); - platformService.getCurrentPlatformVersion = () => "5.1.1"; - 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 platformTemplate = "testPlatformTemplate"; - 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, platformTemplate, 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, platformTemplate, projectData, config, appFilesUpdaterOptions, { skipNativePrepare: true }); - assert.isTrue(areWebpackFilesPersisted); - - // Second execution of `tns preview --bundle` - reset(); - mockPrepareInfo({ nativePlatformStatus: constants.NativePlatformStatus.requiresPlatformAdd }); - await (platformService).ensurePlatformInstalled(platform, platformTemplate, 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, platformTemplate, projectData, config, appFilesUpdaterOptions); - assert.isTrue(areWebpackFilesPersisted); - - // Second execution of `tns run --bundle` - reset(); - mockPrepareInfo({ nativePlatformStatus: constants.NativePlatformStatus.alreadyPrepared }); - await (platformService).ensurePlatformInstalled(platform, platformTemplate, 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, platformTemplate, projectData, config, appFilesUpdaterOptions, { skipNativePrepare: true }); - assert.isTrue(areWebpackFilesPersisted); - - // Execution of `tns run --bundle` - reset(); - mockPrepareInfo({ nativePlatformStatus: constants.NativePlatformStatus.requiresPlatformAdd }); - await (platformService).ensurePlatformInstalled(platform, platformTemplate, projectData, config, appFilesUpdaterOptions); - assert.isTrue(areWebpackFilesPersisted); - - // Execution of `tns preview --bundle` - reset(); - mockPrepareInfo({ nativePlatformStatus: constants.NativePlatformStatus.alreadyPrepared }); - await (platformService).ensurePlatformInstalled(platform, platformTemplate, 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, platformTemplate, projectData, config, appFilesUpdaterOptions, { skipNativePrepare: true }); - assert.isTrue(areWebpackFilesPersisted); - - // Execution of `tns run --bundle` - reset(); - mockPrepareInfo({ nativePlatformStatus: constants.NativePlatformStatus.requiresPlatformAdd }); - await (platformService).ensurePlatformInstalled(platform, platformTemplate, projectData, config, appFilesUpdaterOptions); - assert.isTrue(areWebpackFilesPersisted); - - // Execution of `tns build --bundle` - reset(); - mockPrepareInfo({ nativePlatformStatus: constants.NativePlatformStatus.alreadyPrepared }); - await (platformService).ensurePlatformInstalled(platform, platformTemplate, projectData, config, appFilesUpdaterOptions); - assert.isFalse(areWebpackFilesPersisted); - }); - }); -}); diff --git a/test/plugin-prepare.ts b/test/plugin-prepare.ts deleted file mode 100644 index 00dd7b04b1..0000000000 --- a/test/plugin-prepare.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { assert } from "chai"; -import { NpmPluginPrepare } from "../lib/tools/node-modules/node-modules-dest-copy"; - -require("should"); - -class TestNpmPluginPrepare extends NpmPluginPrepare { - public preparedDependencies: IDictionary = {}; - - constructor(private previouslyPrepared: IDictionary) { - super(null, null, { - getPlatformData: () => { - return { - platformProjectService: { - beforePrepareAllPlugins: () => Promise.resolve() - } - }; - } - }, null); - } - - protected getPreviouslyPreparedDependencies(platform: string): IDictionary { - return this.previouslyPrepared; - } - - protected async afterPrepare(dependencies: IDependencyData[], platform: string): Promise { - _.each(dependencies, d => { - this.preparedDependencies[d.name] = true; - }); - } -} - -describe("Plugin preparation", () => { - it("skips prepare if no plugins", async () => { - const pluginPrepare = new TestNpmPluginPrepare({}); - await pluginPrepare.preparePlugins([], "android", null, {}); - assert.deepEqual({}, pluginPrepare.preparedDependencies); - }); - - it("saves prepared plugins after preparation", async () => { - const pluginPrepare = new TestNpmPluginPrepare({ "tns-core-modules-widgets": true }); - const testDependencies: IDependencyData[] = [ - { - name: "tns-core-modules-widgets", - depth: 0, - directory: "some dir", - nativescript: null, - }, - { - name: "nativescript-calendar", - depth: 0, - directory: "some dir", - nativescript: null, - } - ]; - await pluginPrepare.preparePlugins(testDependencies, "android", null, {}); - const prepareData = { "tns-core-modules-widgets": true, "nativescript-calendar": true }; - assert.deepEqual(prepareData, pluginPrepare.preparedDependencies); - }); -}); diff --git a/test/plugin-variables-service.ts b/test/plugin-variables-service.ts deleted file mode 100644 index 86657459e3..0000000000 --- a/test/plugin-variables-service.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { assert } from "chai"; -import { Errors } from "../lib/common/errors"; -import { FileSystem } from "../lib/common/file-system"; -import { HostInfo } from "../lib/common/host-info"; -import { Options } from "../lib/options"; -import { PluginVariablesHelper } from "../lib/common/plugin-variables-helper"; -import { PluginVariablesService } from "../lib/services/plugin-variables-service"; -import { ProjectData } from "../lib/project-data"; -import { ProjectDataService } from "../lib/services/project-data-service"; -import { ProjectHelper } from "../lib/common/project-helper"; -import { StaticConfig } from "../lib/config"; -import { MessagesService } from "../lib/common/services/messages-service"; -import { Yok } from '../lib/common/yok'; -import { SettingsService } from "../lib/common/test/unit-tests/stubs"; -import { DevicePlatformsConstants } from "../lib/common/mobile/device-platforms-constants"; -import * as stubs from './stubs'; -import * as path from "path"; -import * as temp from "temp"; -temp.track(); - -function createTestInjector(): IInjector { - const testInjector = new Yok(); - - testInjector.register("messagesService", MessagesService); - testInjector.register("errors", Errors); - testInjector.register("fs", FileSystem); - testInjector.register("hostInfo", HostInfo); - testInjector.register("logger", stubs.LoggerStub); - testInjector.register("options", Options); - testInjector.register("pluginVariablesHelper", PluginVariablesHelper); - testInjector.register("pluginVariablesService", PluginVariablesService); - testInjector.register("projectData", ProjectData); - testInjector.register("projectDataService", ProjectDataService); - testInjector.register("projectHelper", ProjectHelper); - testInjector.register("prompter", { - get: () => { - const errors: IErrors = testInjector.resolve("errors"); - errors.fail("$prompter.get function shouldn't be called!"); - } - }); - testInjector.register("staticConfig", StaticConfig); - testInjector.register("settingsService", SettingsService); - testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); - testInjector.register("androidResourcesMigrationService", { - hasMigrated: () => true - }); - - return testInjector; -} - -async function createProjectFile(testInjector: IInjector): Promise { - const tempFolder = temp.mkdirSync("pluginVariablesService"); - - const options = testInjector.resolve("options"); - options.path = tempFolder; - - const projectData = { - "name": "myProject", - "nativescript": { - id: { android: "", ios: ""} - } - }; - testInjector.resolve("fs").writeJson(path.join(tempFolder, "package.json"), projectData); - - return tempFolder; -} - -function createPluginData(pluginVariables: any): IPluginData { - const pluginData = { - name: "myTestPlugin", - version: "", - fullPath: "", - isPlugin: true, - moduleInfo: "", - platformsData: { - ios: "", - android: "" - }, - pluginVariables: pluginVariables, - pluginPlatformsFolderPath: (platform: string) => "" - }; - - return pluginData; -} - -describe("Plugin Variables service", () => { - let testInjector: IInjector; - beforeEach(() => { - testInjector = createTestInjector(); - }); - - describe("plugin add when the console is non interactive", () => { - beforeEach(() => { - const helpers = require("./../lib/common/helpers"); - helpers.isInteractive = () => false; - }); - it("fails when no --var option and no default value are specified", async () => { - await createProjectFile(testInjector); - - const pluginVariables = { "MY_TEST_PLUGIN_VARIABLE": {} }; - const pluginData = createPluginData(pluginVariables); - const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - const projectData: IProjectData = testInjector.resolve("projectData"); - projectData.initializeProjectData(); - - const expectedError = `Unable to find value for MY_TEST_PLUGIN_VARIABLE plugin variable from ${pluginData.name} plugin. Ensure the --var option is specified or the plugin variable has default value.`; - let actualError: string = null; - - try { - await pluginVariablesService.savePluginVariablesInProjectFile(pluginData, projectData.projectDir); - } catch (err) { - actualError = err.message; - } - - assert.equal(expectedError, actualError); - }); - it("does not fail when --var option is specified", async () => { - await createProjectFile(testInjector); - - const pluginVariableValue = "myAppId"; - testInjector.resolve("options").var = { - "MY_APP_ID": pluginVariableValue - }; - - const pluginVariables = { "MY_APP_ID": {} }; - const pluginData = createPluginData(pluginVariables); - const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - const projectData: IProjectData = testInjector.resolve("projectData"); - projectData.initializeProjectData(); - await pluginVariablesService.savePluginVariablesInProjectFile(pluginData, projectData.projectDir); - - const fs = testInjector.resolve("fs"); - const staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); - - const projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")); - assert.equal(pluginVariableValue, projectFileContent[staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][`${pluginData.name}-variables`]["MY_APP_ID"]); - }); - it("does not fail when default value is specified", async () => { - await createProjectFile(testInjector); - - const defaultPluginValue = "myDefaultValue"; - const pluginVariables = { "MY_TEST_PLUGIN_VARIABLE": { defaultValue: defaultPluginValue } }; - const pluginData = createPluginData(pluginVariables); - const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - const projectData = testInjector.resolve("projectData"); - projectData.initializeProjectData(); - - await pluginVariablesService.savePluginVariablesInProjectFile(pluginData, projectData.projectDir); - - const fs = testInjector.resolve("fs"); - const staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); - - const projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")); - assert.equal(defaultPluginValue, projectFileContent[staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][`${pluginData.name}-variables`]["MY_TEST_PLUGIN_VARIABLE"]); - }); - }); - - describe("plugin add when the console is interactive", () => { - beforeEach(() => { - const helpers = require("./../lib/common/helpers"); - helpers.isInteractive = () => true; - }); - it("prompt for plugin variable value when no --var option and no default value are specified", async () => { - await createProjectFile(testInjector); - - const pluginVariableValue = "testAppURL"; - const prompter = testInjector.resolve("prompter"); - prompter.get = async () => ({ "APP_URL": pluginVariableValue }); - - const pluginVariables = { "APP_URL": {} }; - const pluginData = createPluginData(pluginVariables); - const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - const projectData = testInjector.resolve("projectData"); - projectData.initializeProjectData(); - await pluginVariablesService.savePluginVariablesInProjectFile(pluginData, projectData.projectDir); - - const fs = testInjector.resolve("fs"); - const staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); - - const projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")); - assert.equal(pluginVariableValue, projectFileContent[staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][`${pluginData.name}-variables`]["APP_URL"]); - }); - it("does not prompt for plugin variable value when default value is specified", async () => { - await createProjectFile(testInjector); - - const defaultPluginValue = "myAppNAme"; - const pluginVariables = { "APP_NAME": { defaultValue: defaultPluginValue } }; - const pluginData = createPluginData(pluginVariables); - const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - const projectData = testInjector.resolve("projectData"); - projectData.initializeProjectData(); - - await pluginVariablesService.savePluginVariablesInProjectFile(pluginData, projectData.projectDir); - - const fs = testInjector.resolve("fs"); - const staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); - - const projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")); - assert.equal(defaultPluginValue, projectFileContent[staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][`${pluginData.name}-variables`]["APP_NAME"]); - }); - it("does not prompt for plugin variable value when --var option is specified", async () => { - await createProjectFile(testInjector); - - const pluginVariableValue = "pencho.goshko"; - testInjector.resolve("options").var = { - "USERNAME": pluginVariableValue - }; - - const pluginVariables = { "USERNAME": {} }; - const pluginData = createPluginData(pluginVariables); - const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - const projectData = testInjector.resolve("projectData"); - projectData.initializeProjectData(); - await pluginVariablesService.savePluginVariablesInProjectFile(pluginData, projectData.projectDir); - - const fs = testInjector.resolve("fs"); - const staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); - - const projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")); - assert.equal(pluginVariableValue, projectFileContent[staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][`${pluginData.name}-variables`]["USERNAME"]); - }); - }); - - describe("plugin interpolation", () => { - it("fails when the plugin value is undefined", async () => { - const tempFolder = await createProjectFile(testInjector); - - const pluginVariables = { "MY_VAR": {} }; - const pluginData = createPluginData(pluginVariables); - - const fs: IFileSystem = testInjector.resolve("fs"); - const filePath = path.join(tempFolder, "myfile"); - fs.writeFile(filePath, ""); - - const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - const projectData = testInjector.resolve("projectData"); - projectData.initializeProjectData(); - - const expectedError = "Unable to find the value for MY_VAR plugin variable into project package.json file. Verify that your package.json file is correct and try again."; - let error: string = null; - try { - await pluginVariablesService.interpolatePluginVariables(pluginData, filePath, projectData.projectDir); - } catch (err) { - error = err.message; - } - - assert.equal(error, expectedError); - }); - - it("interpolates correctly plugin variable value", async () => { - const tempFolder = await createProjectFile(testInjector); - - const projectData: IProjectData = testInjector.resolve("projectData"); - projectData.initializeProjectData(); - const fs: IFileSystem = testInjector.resolve("fs"); - - // Write plugin variables values to package.json file - const packageJsonFilePath = path.join(projectData.projectDir, "package.json"); - const data = fs.readJson(packageJsonFilePath); - data["nativescript"]["myTestPlugin-variables"] = { - "FB_APP_NAME": "myFacebookAppName" - }; - fs.writeJson(packageJsonFilePath, data); - - const pluginVariables = { "FB_APP_NAME": {} }; - const pluginData = createPluginData(pluginVariables); - const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - const pluginConfigurationFileContent = '' + - '' + - '' + - '' + - '' + - ''; - const filePath = path.join(tempFolder, "myfile"); - fs.writeFile(filePath, pluginConfigurationFileContent); - - await pluginVariablesService.interpolatePluginVariables(pluginData, filePath, projectData.projectDir); - - const result = fs.readText(filePath); - const expectedResult = '' + - '' + - '' + - '' + - '' + - ''; - - assert.equal(result, expectedResult); - }); - - it("interpolates correctly case sensive plugin variable value", async () => { - const tempFolder = await createProjectFile(testInjector); - - const projectData: IProjectData = testInjector.resolve("projectData"); - projectData.initializeProjectData(); - const fs: IFileSystem = testInjector.resolve("fs"); - - // Write plugin variables values to package.json file - const packageJsonFilePath = path.join(projectData.projectDir, "package.json"); - const data = fs.readJson(packageJsonFilePath); - data["nativescript"]["myTestPlugin-variables"] = { - "FB_APP_NAME": "myFacebookAppName" - }; - fs.writeJson(packageJsonFilePath, data); - - const pluginVariables = { "FB_APP_NAME": {} }; - const pluginData = createPluginData(pluginVariables); - const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - const pluginConfigurationFileContent = '' + - '' + - '' + - '' + - '' + - ''; - const filePath = path.join(tempFolder, "myfile"); - fs.writeFile(filePath, pluginConfigurationFileContent); - - await pluginVariablesService.interpolatePluginVariables(pluginData, filePath, projectData.projectDir); - - const result = fs.readText(filePath); - const expectedResult = '' + - '' + - '' + - '' + - '' + - ''; - - assert.equal(result, expectedResult); - }); - - it("interpolates correctly more than one plugin variables values", async () => { - const tempFolder = await createProjectFile(testInjector); - - const projectData: IProjectData = testInjector.resolve("projectData"); - projectData.initializeProjectData(); - const fs: IFileSystem = testInjector.resolve("fs"); - - const packageJsonFilePath = path.join(projectData.projectDir, "package.json"); - const data = fs.readJson(packageJsonFilePath); - data["nativescript"]["myTestPlugin-variables"] = { - "FB_APP_NAME": "myFacebookAppName", - "FB_APP_URL": "myFacebookAppURl" - }; - fs.writeJson(packageJsonFilePath, data); - - const pluginVariables = { "FB_APP_NAME": {}, "FB_APP_URL": {} }; - const pluginData = createPluginData(pluginVariables); - const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - const pluginConfigurationFileContent = '' + - '' + - '' + - '' + - '' + - '' + - ''; - const filePath = path.join(tempFolder, "myfile"); - fs.writeFile(filePath, pluginConfigurationFileContent); - - await pluginVariablesService.interpolatePluginVariables(pluginData, filePath, projectData.projectDir); - - const result = fs.readText(filePath); - const expectedResult = '' + - '' + - '' + - '' + - '' + - '' + - ''; - - assert.equal(result, expectedResult); - }); - }); -}); diff --git a/test/plugins-service.ts b/test/plugins-service.ts index e36966e1aa..cb44b1436f 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"; @@ -35,6 +34,9 @@ import StaticConfigLib = require("../lib/config"); import * as path from "path"; import * as temp from "temp"; import { PLUGINS_BUILD_DATA_FILENAME } from '../lib/constants'; +import { GradleCommandService } from '../lib/services/android/gradle-command-service'; +import { GradleBuildService } from '../lib/services/android/gradle-build-service'; +import { GradleBuildArgsService } from '../lib/services/android/gradle-build-args-service'; temp.track(); let isErrorThrown = false; @@ -52,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", {}); @@ -143,7 +144,9 @@ function createTestInjector() { }, extractPackage: async (packageName: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise => undefined }); - + testInjector.register("gradleCommandService", GradleCommandService); + testInjector.register("gradleBuildService", GradleBuildService); + testInjector.register("gradleBuildArgsService", GradleBuildArgsService); testInjector.register("cleanupService", { setShouldDispose: (shouldDispose: boolean): void => undefined }); @@ -310,9 +313,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", @@ -534,9 +537,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", @@ -567,7 +570,7 @@ describe("Plugins service", () => { `\n@#[line:1,col:39].` + `\n@#[line:1,col:39].`; mockBeginCommand(testInjector, expectedErrorMessage); - await pluginsService.prepare(pluginJsonData, "android", projectData, {}); + await pluginsService.preparePluginNativeCode(pluginsService.convertToPluginData(pluginJsonData, projectData.projectDir), "android", projectData); }); }); @@ -580,7 +583,7 @@ describe("Plugins service", () => { }; const unitTestsInjector = new Yok(); - unitTestsInjector.register("platformsData", { + unitTestsInjector.register("platformsDataService", { getPlatformData: (_platform: string, pData: IProjectData) => ({ projectRoot: "projectRoot", platformProjectService: { @@ -626,6 +629,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..737e3e66c5 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); @@ -36,6 +36,7 @@ class ProjectChangesServiceTest extends BaseServiceTest { }); this.injector.register("logger", LoggerStub); this.injector.register("hooksService", HooksServiceStub); + this.injector.register("nodeModulesDependenciesBuilder", {}); const fs = this.injector.resolve("fs"); fs.writeJson(path.join(this.projectDir, Constants.PACKAGE_JSON_FILE_NAME), { @@ -54,8 +55,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 +81,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 +99,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 +111,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 +125,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 +151,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 +161,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", () => { + it("creates prepare info and sets only the native platform status when there isn't an existing prepare info", async () => { for (const platform of ["ios", "android"]) { - serviceTest.projectChangesService.setNativePlatformStatus(platform, serviceTest.projectData, { nativePlatformStatus: Constants.NativePlatformStatus.requiresPrepare }); + await serviceTest.projectChangesService.setNativePlatformStatus(serviceTest.getPlatformData(platform), serviceTest.projectData, { 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 +182,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, {}); + await serviceTest.projectChangesService.savePrepareInfo(serviceTest.getPlatformData(platform), serviceTest.projectData, null); + const prepareInfo = serviceTest.projectChangesService.getPrepareInfo(serviceTest.getPlatformData(platform)); + + await serviceTest.projectChangesService.setNativePlatformStatus(serviceTest.getPlatformData(platform), serviceTest.projectData, { nativePlatformStatus: Constants.NativePlatformStatus.alreadyPrepared }); + + const actualPrepareInfo = serviceTest.projectChangesService.getPrepareInfo(serviceTest.getPlatformData(platform)); prepareInfo.nativePlatformStatus = Constants.NativePlatformStatus.alreadyPrepared; assert.deepEqual(actualPrepareInfo, prepareInfo); } @@ -215,20 +197,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, {}); + await serviceTest.projectChangesService.setNativePlatformStatus(serviceTest.getPlatformData(platform), serviceTest.projectData, { 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-device-debug-service.ts b/test/services/android-device-debug-service.ts index e3b208008c..438fa77c5e 100644 --- a/test/services/android-device-debug-service.ts +++ b/test/services/android-device-debug-service.ts @@ -7,12 +7,14 @@ const expectedDevToolsCommitSha = "02e6bde1bbe34e43b309d4ef774b1168d25fd024"; class AndroidDeviceDebugServiceInheritor extends AndroidDeviceDebugService { constructor(protected $devicesService: Mobile.IDevicesService, + $cleanupService: ICleanupService, $errors: IErrors, $logger: ILogger, $androidProcessService: Mobile.IAndroidProcessService, + $staticConfig: IStaticConfig, $net: INet, $deviceLogProvider: Mobile.IDeviceLogProvider) { - super({ deviceInfo: { identifier: "123" } }, $devicesService, $errors, $logger, $androidProcessService, $net, $deviceLogProvider); + super({ deviceInfo: { identifier: "123" } }, $devicesService, $cleanupService, $errors, $logger, $androidProcessService, $staticConfig, $net, $deviceLogProvider); } public getChromeDebugUrl(debugOptions: IDebugOptions, port: number): string { 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 8d33c84642..96bdc0a128 100644 --- a/test/services/android-project-service.ts +++ b/test/services/android-project-service.ts @@ -3,6 +3,9 @@ import { Yok } from "../../lib/common/yok"; import * as stubs from "../stubs"; import { assert } from "chai"; import * as sinon from "sinon"; +import { GradleCommandService } from "../../lib/services/android/gradle-command-service"; +import { GradleBuildService } from "../../lib/services/android/gradle-build-service"; +import { GradleBuildArgsService } from "../../lib/services/android/gradle-build-args-service"; const createTestInjector = (): IInjector => { const testInjector = new Yok(); @@ -35,6 +38,9 @@ const createTestInjector = (): IInjector => { return true; } }); + testInjector.register("gradleCommandService", GradleCommandService); + testInjector.register("gradleBuildService", GradleBuildService); + testInjector.register("gradleBuildArgsService", GradleBuildArgsService); return testInjector; }; @@ -88,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"); @@ -100,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"); @@ -112,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"); @@ -125,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/android/gradle-build-args-service.ts b/test/services/android/gradle-build-args-service.ts new file mode 100644 index 0000000000..7c23bdb2cd --- /dev/null +++ b/test/services/android/gradle-build-args-service.ts @@ -0,0 +1,162 @@ +import { Yok } from "../../../lib/common/yok"; +import { GradleBuildArgsService } from "../../../lib/services/android/gradle-build-args-service"; +import { assert } from "chai"; + +function createTestInjector(): IInjector { + const injector = new Yok(); + injector.register("androidToolsInfo", { + getToolsInfo: () => ({ + compileSdkVersion: 28, + targetSdkVersion: 26, + buildToolsVersion: "my-build-tools-version", + generateTypings: true + }) + }); + injector.register("logger", {}); + injector.register("gradleBuildArgsService", GradleBuildArgsService); + + return injector; +} + +function executeTests(testCases: any[], testFunction: (gradleBuildArgsService: IGradleBuildArgsService, buildData: IAndroidBuildData) => string[]) { + _.each(testCases, testCase => { + it(testCase.name, () => { + const injector = createTestInjector(); + if (testCase.logLevel) { + const logger = injector.resolve("logger"); + logger.getLevel = () => testCase.logLevel; + } + + const gradleBuildArgsService = injector.resolve("gradleBuildArgsService"); + const args = testFunction(gradleBuildArgsService, testCase.buildConfig); + + assert.deepEqual(args, testCase.expectedResult); + }); + }); +} + +const expectedInfoLoggingArgs = ["--quiet"]; +const expectedTraceLoggingArgs = ["--stacktrace", "--debug"]; +const expectedDebugBuildArgs = ["-PcompileSdk=android-28", "-PtargetSdk=26", "-PbuildToolsVersion=my-build-tools-version", "-PgenerateTypings=true"]; +const expectedReleaseBuildArgs = expectedDebugBuildArgs.concat(["-Prelease", "-PksPath=/my/key/store/path", + "-Palias=keyStoreAlias", "-Ppassword=keyStoreAliasPassword", "-PksPassword=keyStorePassword"]); + +const releaseBuildConfig = { + release: true, + keyStorePath: "/my/key/store/path", + keyStoreAlias: "keyStoreAlias", + keyStoreAliasPassword: "keyStoreAliasPassword", + keyStorePassword: "keyStorePassword" +}; + +describe("GradleBuildArgsService", () => { + describe("getBuildTaskArgs", () => { + const testCases = [ + { + name: "should return correct args for debug build with info log", + buildConfig: { release: false }, + logLevel: "INFO", + expectedResult: ["assembleDebug"].concat(expectedInfoLoggingArgs).concat(expectedDebugBuildArgs) + }, + { + name: "should return correct args for debug build with trace log", + buildConfig: { release: false }, + logLevel: "TRACE", + expectedResult: ["assembleDebug"].concat(expectedTraceLoggingArgs).concat(expectedDebugBuildArgs) + }, + { + name: "should return correct args for release build with info log", + buildConfig: releaseBuildConfig, + logLevel: "INFO", + expectedResult: ["assembleRelease"].concat(expectedInfoLoggingArgs).concat(expectedReleaseBuildArgs) + }, + { + name: "should return correct args for release build with trace log", + buildConfig: releaseBuildConfig, + logLevel: "TRACE", + expectedResult: ["assembleRelease"].concat(expectedTraceLoggingArgs).concat(expectedReleaseBuildArgs) + }, + { + name: "should return correct args for debug build with info log and android bundle", + buildConfig: { release: false, androidBundle: true }, + logLevel: "INFO", + expectedResult: ["bundleDebug"].concat(expectedInfoLoggingArgs).concat(expectedDebugBuildArgs) + }, + { + name: "should return correct args for debug build with trace log and android bundle", + buildConfig: { release: false, androidBundle: true }, + logLevel: "TRACE", + expectedResult: ["bundleDebug"].concat(expectedTraceLoggingArgs).concat(expectedDebugBuildArgs) + }, + { + name: "should return correct args for release build with info log and android bundle", + buildConfig: { ...releaseBuildConfig, androidBundle: true }, + logLevel: "INFO", + expectedResult: ["bundleRelease"].concat(expectedInfoLoggingArgs).concat(expectedReleaseBuildArgs) + }, + { + name: "should return correct args for release build with trace log and android bundle", + buildConfig: { ...releaseBuildConfig, androidBundle: true }, + logLevel: "TRACE", + expectedResult: ["bundleRelease"].concat(expectedTraceLoggingArgs).concat(expectedReleaseBuildArgs) + } + ]; + + executeTests(testCases, (gradleBuildArgsService: IGradleBuildArgsService, buildData: IAndroidBuildData) => gradleBuildArgsService.getBuildTaskArgs(buildData)); + }); + + describe("getCleanTaskArgs", () => { + const testCases = [ + { + name: "should return correct args for debug clean build with info log", + buildConfig: { release: false }, + logLevel: "INFO", + expectedResult: ["clean"].concat(expectedInfoLoggingArgs).concat(expectedDebugBuildArgs) + }, + { + name: "should return correct args for debug clean build with trace log", + buildConfig: { release: false }, + logLevel: "TRACE", + expectedResult: ["clean"].concat(expectedTraceLoggingArgs).concat(expectedDebugBuildArgs) + }, + { + name: "should return correct args for release clean build with info log", + buildConfig: releaseBuildConfig, + logLevel: "INFO", + expectedResult: ["clean"].concat(expectedInfoLoggingArgs).concat(expectedReleaseBuildArgs) + }, + { + name: "should return correct args for release clean build with trace log", + buildConfig: releaseBuildConfig, + logLevel: "TRACE", + expectedResult: ["clean"].concat(expectedTraceLoggingArgs).concat(expectedReleaseBuildArgs) + }, + { + name: "should return correct args for debug clean build with info log and android bundle", + buildConfig: { release: false, androidBundle: true }, + logLevel: "INFO", + expectedResult: ["clean"].concat(expectedInfoLoggingArgs).concat(expectedDebugBuildArgs) + }, + { + name: "should return correct args for debug clean build with trace log and android bundle", + buildConfig: { release: false, androidBundle: true }, + logLevel: "TRACE", + expectedResult: ["clean"].concat(expectedTraceLoggingArgs).concat(expectedDebugBuildArgs) + }, + { + name: "should return correct args for release clean build with info log and android bundle", + buildConfig: { ...releaseBuildConfig, androidBundle: true }, + logLevel: "INFO", + expectedResult: ["clean"].concat(expectedInfoLoggingArgs).concat(expectedReleaseBuildArgs) + }, + { + name: "should return correct args for release clean build with trace log and android bundle", + buildConfig: { ...releaseBuildConfig, androidBundle: true }, + logLevel: "TRACE", + expectedResult: ["clean"].concat(expectedTraceLoggingArgs).concat(expectedReleaseBuildArgs) + } + ]; + + executeTests(testCases, (gradleBuildArgsService: IGradleBuildArgsService, buildData: IAndroidBuildData) => gradleBuildArgsService.getCleanTaskArgs(buildData)); + }); +}); diff --git a/test/services/ios-device-debug-service.ts b/test/services/ios-device-debug-service.ts index d97f37863d..cff0409d54 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", {}); @@ -189,4 +188,14 @@ describe("iOSDeviceDebugService", () => { } }); + describe("validate", () => { + it("the OS is neither Windows or macOS and device is iOS", async () => { + const testInjector = createTestInjector(); + const hostInfo = testInjector.resolve("hostInfo"); + hostInfo.isDarwin = hostInfo.isWindows = false; + + const iOSDeviceDebugService = testInjector.resolve(IOSDeviceDebugServiceInheritor); + assert.isRejected(iOSDeviceDebugService.debug(null, null), "Debugging on iOS devices is not supported for"); + }); + }); }); diff --git a/test/services/ios/export-options-plist-service.ts b/test/services/ios/export-options-plist-service.ts new file mode 100644 index 0000000000..e92c6e5a41 --- /dev/null +++ b/test/services/ios/export-options-plist-service.ts @@ -0,0 +1,106 @@ +import { Yok } from "../../../lib/common/yok"; +import { ExportOptionsPlistService } from "../../../lib/services/ios/export-options-plist-service"; +import { assert } from "chai"; +import { EOL } from "os"; + +let actualPlistTemplate: string = null; +const projectName = "myProjectName"; +const projectRoot = "/my/test/project/platforms/ios"; +const archivePath = "/my/test/archive/path"; + +function createTestInjector() { + const injector = new Yok(); + injector.register("fs", { + writeFile: (exportPath: string, plistTemplate: string) => { + actualPlistTemplate = plistTemplate; + } + }); + injector.register("exportOptionsPlistService", ExportOptionsPlistService); + + return injector; +} + +describe("ExportOptionsPlistService", () => { + describe("createDevelopmentExportOptionsPlist", () => { + const testCases = [ + { + name: "should create default export options plist", + buildConfig: {} + }, + { + name: "should create export options plist with provision", + buildConfig: { provision: "myTestProvision" }, + expectedPlist: "provisioningProfiles org.nativescript.myTestApp myTestProvision " + }, + { + name: "should create export options plist with mobileProvisionIdentifier", + buildConfig: { mobileProvisionIdentifier: "myTestProvision" }, + expectedPlist: "provisioningProfiles org.nativescript.myTestApp myTestProvision " + } + ]; + + _.each(testCases, testCase => { + _.each(["Development", "AdHoc", "Distribution", "Enterprise"], provisionType => { + it(testCase.name, () => { + const injector = createTestInjector(); + const exportOptionsPlistService = injector.resolve("exportOptionsPlistService"); + exportOptionsPlistService.getExportOptionsMethod = () => provisionType; + + const projectData = { projectName, projectIdentifiers: { ios: "org.nativescript.myTestApp" }}; + exportOptionsPlistService.createDevelopmentExportOptionsPlist(archivePath, projectData, testCase.buildConfig); + + const template = actualPlistTemplate.split(EOL).join(" "); + assert.isTrue(template.indexOf(`method ${provisionType}`) > 0); + assert.isTrue(template.indexOf("uploadBitcode ") > 0); + assert.isTrue(template.indexOf("compileBitcode ") > 0); + if (testCase.expectedPlist) { + assert.isTrue(template.indexOf(testCase.expectedPlist) > 0); + } + }); + }); + }); + }); + describe("createDistributionExportOptionsPlist", () => { + const testCases = [ + { + name: "should create default export options plist", + buildConfig: {} + }, + { + name: "should create export options plist with provision", + buildConfig: { provision: "myTestProvision" }, + expectedPlist: "provisioningProfiles org.nativescript.myTestApp myTestProvision " + }, + { + name: "should create export options plist with mobileProvisionIdentifier", + buildConfig: { mobileProvisionIdentifier: "myTestProvision" }, + expectedPlist: "provisioningProfiles org.nativescript.myTestApp myTestProvision " + }, + { + name: "should create export options plist with teamID", + buildConfig: { teamId: "myTeamId" }, + expectedPlist: "teamID myTeamId" + } + ]; + + _.each(testCases, testCase => { + it(testCase.name, () => { + const injector = createTestInjector(); + const exportOptionsPlistService = injector.resolve("exportOptionsPlistService"); + exportOptionsPlistService.getExportOptionsMethod = () => "app-store"; + + const projectData = { projectName, projectIdentifiers: { ios: "org.nativescript.myTestApp" }}; + exportOptionsPlistService.createDistributionExportOptionsPlist(projectRoot, projectData, testCase.buildConfig); + + const template = actualPlistTemplate.split(EOL).join(" "); + assert.isTrue(template.indexOf("method app-store") > 0); + assert.isTrue(template.indexOf("uploadBitcode ") > 0); + assert.isTrue(template.indexOf("compileBitcode ") > 0); + assert.isTrue(template.indexOf("uploadSymbols ") > 0); + if (testCase.expectedPlist) { + assert.isTrue(template.indexOf(testCase.expectedPlist) > 0); + } + }); + }); + }); +}); diff --git a/test/services/ios/ios-signing-service.ts b/test/services/ios/ios-signing-service.ts new file mode 100644 index 0000000000..4fb176bcea --- /dev/null +++ b/test/services/ios/ios-signing-service.ts @@ -0,0 +1,330 @@ +import { Yok } from "../../../lib/common/yok"; +import { IOSSigningService } from "../../../lib/services/ios/ios-signing-service"; +import { assert } from "chai"; +import { ManualSigning } from "pbxproj-dom/xcode"; +import { Errors } from "../../../lib/common/errors"; + +interface IXcodeMock { + isSetManualSigningStyleCalled: boolean; + isSetAutomaticSigningStyleCalled: boolean; + isSetManualSigningStyleByTargetProductTypesListCalled: boolean; + isSetAutomaticSigningStyleByTargetProductTypesListCalled: boolean; + isSaveCalled: boolean; +} + +const projectRoot = "myProjectRoot"; +const teamId = "myTeamId"; +const projectData: any = { + projectName: "myProjectName", + appResourcesDirectoryPath: "app-resources/path", + projectIdentifiers: { + ios: "org.nativescript.testApp" + } +}; +const NativeScriptDev = { + Name: "NativeScriptDev", + TeamName: "Telerik AD", + TeamIdentifier: ["TKID101"], + Entitlements: { + "application-identifier": "*", + "com.apple.developer.team-identifier": "ABC" + }, + UUID: "12345", + ProvisionsAllDevices: false, + Type: "Development" +}; +const NativeScriptDist = { + Name: "NativeScriptDist", + TeamName: "Telerik AD", + TeamIdentifier: ["TKID202"], + Entitlements: { + "application-identifier": "*", + "com.apple.developer.team-identifier": "ABC" + }, + UUID: "6789", + ProvisionsAllDevices: true, + Type: "Distribution" +}; +const NativeScriptAdHoc = { + Name: "NativeScriptAdHoc", + TeamName: "Telerik AD", + TeamIdentifier: ["TKID303"], + Entitlements: { + "application-identifier": "*", + "com.apple.developer.team-identifier": "ABC" + }, + UUID: "1010", + ProvisionsAllDevices: true, + Type: "Distribution" +}; + +class XcodeMock implements IXcodeMock { + public isSetManualSigningStyleCalled = false; + public isSetAutomaticSigningStyleCalled = false; + public isSetManualSigningStyleByTargetProductTypesListCalled = false; + public isSetAutomaticSigningStyleByTargetProductTypesListCalled = false; + public isSaveCalled = false; + + constructor(private data: { signing: { style: string, team?: string } }) { } + + public getSigning() { + return this.data.signing; + } + + public setManualSigningStyle(projectName: string, configuration: ManualSigning) { + this.isSetManualSigningStyleCalled = true; + } + + public setAutomaticSigningStyle() { + this.isSetAutomaticSigningStyleCalled = true; + } + + public setManualSigningStyleByTargetProductTypesList() { + this.isSetManualSigningStyleByTargetProductTypesListCalled = true; + } + + public setAutomaticSigningStyleByTargetProductTypesList() { + this.isSetAutomaticSigningStyleByTargetProductTypesListCalled = true; + } + + public save() { + this.isSaveCalled = true; + } +} + +function setup(data: { + hasXCConfigrovisioning?: boolean, + hasXCConfigDevelopmentTeam?: boolean, + signing?: { style: string }, + teamIdsForName?: string[], + provision?: string +}): { injector: IInjector, xcodeMock: any } { + const { hasXCConfigrovisioning, hasXCConfigDevelopmentTeam, signing, teamIdsForName, provision = "myProvision" } = data; + const xcodeMock = new XcodeMock({ signing }); + + const injector = new Yok(); + injector.register("errors", Errors); + injector.register("fs", {}); + injector.register("iOSProvisionService", { + getTeamIdsWithName: () => teamIdsForName || [], + pick: async (uuidOrName: string, projId: string) => { + return ({ + NativeScriptDev, + NativeScriptDist, + NativeScriptAdHoc + })[uuidOrName]; + } + }); + injector.register("logger", { + trace: () => ({}) + }); + injector.register("pbxprojDomXcode", { + Xcode: { + open: () => xcodeMock + } + }); + injector.register("prompter", {}); + injector.register("xcconfigService", { + readPropertyValue: (xcconfigFilePath: string, propertyName: string) => { + if (propertyName.startsWith("PROVISIONING_PROFILE")) { + return hasXCConfigrovisioning ? provision : null; + } + if (propertyName.startsWith("DEVELOPMENT_TEAM")) { + return hasXCConfigDevelopmentTeam ? teamId : null; + } + } + }); + injector.register("xcprojService", { + getXcodeprojPath: () => "some/path" + }); + injector.register("iOSSigningService", IOSSigningService); + + return { injector, xcodeMock }; +} + +describe("IOSSigningService", () => { + describe("setupSigningForDevice", () => { + const testCases = [ + { + name: "should sign the project manually when PROVISIONING_PROFILE is provided from xcconfig and the project is still not signed", + arrangeData: { hasXCConfigrovisioning: true, signing: null }, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetManualSigningStyleCalled); + assert.isTrue(xcodeMock.isSaveCalled); + } + }, + { + name: "should sign the project manually when PROVISIONING_PROFILE is provided from xcconfig and the project is automatically signed", + arrangeData: { hasXCConfigrovisioning: true, signing: { style: "Automatic" } }, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetManualSigningStyleCalled); + assert.isTrue(xcodeMock.isSaveCalled); + } + }, + { + name: "shouldn't sign the project manually when PROVISIONING_PROFILE is provided from xcconfig and the project is already manually signed", + arrangeData: { hasXCConfigrovisioning: true, signing: { style: "Manual" } }, + assert: (xcodeMock: IXcodeMock) => { + assert.isFalse(xcodeMock.isSetManualSigningStyleCalled); + assert.isFalse(xcodeMock.isSaveCalled); + } + }, + { + name: "should sign the project automatically when PROVISIONING_PROFILE is not provided from xcconfig, DEVELOPMENT_TEAM is provided from xcconfig and the project is still not signed", + arrangeData: { hasXCConfigrovisioning: false, hasXCConfigDevelopmentTeam: true, signing: null }, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleCalled); + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleByTargetProductTypesListCalled); + assert.isTrue(xcodeMock.isSaveCalled); + } + }, + { + name: "should sign the project automatically when PROVISIONING_PROFILE is not provided from xcconfig, DEVELOPMENT_TEAM is provided from xcconfig and the project is automatically signed", + arrangeData: { hasXCConfigrovisioning: false, hasXCConfigDevelopmentTeam: true, signing: { style: "Automatic" } }, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleCalled); + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleByTargetProductTypesListCalled); + assert.isTrue(xcodeMock.isSaveCalled); + } + }, + { + name: "shouldn't sign the project when PROVISIONING_PROFILE is not provided from xcconfig, DEVELOPMENT_TEAM is provided from xcconfig and the project is already manually signed", + arrangeData: { hasXCConfigrovisioning: false, hasXCConfigDevelopmentTeam: true, signing: { style: "Manual" } }, + assert: (xcodeMock: IXcodeMock) => { + assert.isFalse(xcodeMock.isSetAutomaticSigningStyleCalled); + assert.isFalse(xcodeMock.isSetAutomaticSigningStyleByTargetProductTypesListCalled); + assert.isFalse(xcodeMock.isSetManualSigningStyleCalled); + assert.isFalse(xcodeMock.isSaveCalled); + } + } + ]; + + _.each(testCases, testCase => { + it(testCase.name, async () => { + const { injector, xcodeMock } = setup(testCase.arrangeData); + + const iOSSigningService = injector.resolve("iOSSigningService"); + await iOSSigningService.setupSigningForDevice(projectRoot, projectData, (testCase).buildConfig || {}); + + testCase.assert(xcodeMock); + }); + }); + }); + describe("setupSigningFromTeam", () => { + const testCases = [ + { + name: "should sign the project for given teamId when the project is still not signed", + arrangeData: { signing: null }, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleCalled); + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleByTargetProductTypesListCalled); + assert.isTrue(xcodeMock.isSaveCalled); + assert.isFalse(xcodeMock.isSetManualSigningStyleCalled); + } + }, + { + name: "should sign the project for given teamId when the project is already automatically signed for another team", + arrangeData: { signing: { style: "Automatic" } }, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleCalled); + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleByTargetProductTypesListCalled); + assert.isTrue(xcodeMock.isSaveCalled); + assert.isFalse(xcodeMock.isSetManualSigningStyleCalled); + } + }, + { + name: "shouldn't sign the project for given teamId when the project is already automatically signed for this team", + arrangeData: { signing: { style: "Automatic", team: teamId }}, + assert: (xcodeMock: IXcodeMock) => { + assert.isFalse(xcodeMock.isSetAutomaticSigningStyleCalled); + assert.isFalse(xcodeMock.isSetAutomaticSigningStyleByTargetProductTypesListCalled); + assert.isFalse(xcodeMock.isSaveCalled); + assert.isFalse(xcodeMock.isSetManualSigningStyleCalled); + } + }, + { + name: "shouldn't sign the project for given teamName when the project is already automatically signed for this team", + arrangeData: { signing: { style: "Automatic", team: "anotherTeamId" }, teamIdsForName: [ "anotherTeamId" ] }, + assert: (xcodeMock: IXcodeMock) => { + assert.isFalse(xcodeMock.isSetAutomaticSigningStyleCalled); + assert.isFalse(xcodeMock.isSetAutomaticSigningStyleByTargetProductTypesListCalled); + assert.isFalse(xcodeMock.isSaveCalled); + assert.isFalse(xcodeMock.isSetManualSigningStyleCalled); + } + }, + { + name: "should set automatic signing style when the project is already manually signed", + arrangeData: { signing: { style: "Manual" }}, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleCalled); + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleByTargetProductTypesListCalled); + assert.isTrue(xcodeMock.isSaveCalled); + assert.isFalse(xcodeMock.isSetManualSigningStyleCalled); + } + } + ]; + + _.each(testCases, testCase => { + it(testCase.name, async () => { + const { injector, xcodeMock } = setup(testCase.arrangeData); + + const iOSSigningService: IiOSSigningService = injector.resolve("iOSSigningService"); + await iOSSigningService.setupSigningFromTeam(projectRoot, projectData, teamId); + + testCase.assert(xcodeMock); + }); + }); + }); + describe("setupSigningFromProvision", () => { + const testCases = [ + { + name: "should sign the project manually when it is still not signed", + arrangeData: { signing: null }, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetManualSigningStyleCalled); + assert.isTrue(xcodeMock.isSetManualSigningStyleByTargetProductTypesListCalled); + assert.isTrue(xcodeMock.isSaveCalled); + } + }, + { + name: "should sign the project manually when it is automatically signed", + arrangeData: { signing: { style: "Automatic" } }, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetManualSigningStyleCalled); + assert.isTrue(xcodeMock.isSetManualSigningStyleByTargetProductTypesListCalled); + assert.isTrue(xcodeMock.isSaveCalled); + } + }, + { + name: "shouldn't sign the project when it is already manual signed", + arrangeData: { signing: { style: "Manual" } }, + assert: (xcodeMock: IXcodeMock) => { + assert.isFalse(xcodeMock.isSetManualSigningStyleCalled); + assert.isFalse(xcodeMock.isSetManualSigningStyleByTargetProductTypesListCalled); + assert.isFalse(xcodeMock.isSaveCalled); + } + } + ]; + + _.each(testCases, testCase => { + _.each(["NativeScriptDev", "NativeScriptDist", "NativeScriptAdHoc"], provision => { + it(`${testCase.name} for ${provision} provision`, async () => { + const { injector, xcodeMock } = setup(testCase.arrangeData); + + const iOSSigningService: IiOSSigningService = injector.resolve("iOSSigningService"); + await iOSSigningService.setupSigningFromProvision(projectRoot, projectData, provision); + + testCase.assert(xcodeMock); + }); + }); + }); + + it("should throw an error when no mobileProvisionData", async () => { + const provision = "myTestProvision"; + const { injector } = setup({ signing: null }); + + const iOSSigningService: IiOSSigningService = injector.resolve("iOSSigningService"); + assert.isRejected(iOSSigningService.setupSigningFromProvision(projectRoot, projectData, provision), `Failed to find mobile provision with UUID or Name: ${provision}`); + }); + }); +}); diff --git a/test/services/ios/xcodebuild-args-service.ts b/test/services/ios/xcodebuild-args-service.ts new file mode 100644 index 0000000000..8a284347e0 --- /dev/null +++ b/test/services/ios/xcodebuild-args-service.ts @@ -0,0 +1,135 @@ +import { Yok } from "../../../lib/common/yok"; +import { DevicePlatformsConstants } from "../../../lib/common/mobile/device-platforms-constants"; +import { XcodebuildArgsService } from "../../../lib/services/ios/xcodebuild-args-service"; +import * as path from "path"; +import { assert } from "chai"; + +function createTestInjector(data: { logLevel: string, hasProjectWorkspace: boolean, connectedDevices?: any[] }): IInjector { + const injector = new Yok(); + injector.register("devicePlatformsConstants", DevicePlatformsConstants); + injector.register("devicesService", { + initialize: async () => ({}), + getDevicesForPlatform: () => data.connectedDevices || [] + }); + injector.register("fs", { + exists: () => data.hasProjectWorkspace + }); + injector.register("logger", { + getLevel: () => data.logLevel + }); + injector.register("xcodebuildArgsService", XcodebuildArgsService); + injector.register("iOSWatchAppService", { + hasWatchApp: () => false + }); + + return injector; +} + +const projectRoot = "path/to/my/app/folder/platforms/ios"; +const projectName = "myApp"; +const buildOutputPath = path.join(projectRoot, projectName, "archive"); + +function getCommonArgs() { + return [ + "BUILD_DIR=" + path.join(projectRoot, "build"), + "SHARED_PRECOMPS_DIR=" + path.join(projectRoot, 'build', 'sharedpch'), + "-allowProvisioningUpdates" + ]; +} + +function getXcodeProjectArgs(data?: { hasProjectWorkspace: boolean, hasTarget?: boolean }) { + return data && data.hasProjectWorkspace ? [ + "-workspace", path.join(projectRoot, `${projectName}.xcworkspace`), + "-scheme", projectName + ] : [ + "-project", path.join(projectRoot, `${projectName}.xcodeproj`), + data && data.hasTarget ? "-target" : "-scheme", projectName + ]; +} + +function getBuildLoggingArgs(logLevel: string): string[] { + if (logLevel === "INFO") { + return ["-quiet"]; + } + + return []; +} + +describe("xcodebuildArgsService", () => { + describe("getBuildForSimulatorArgs", () => { + _.each([true, false], hasProjectWorkspace => { + _.each(["INFO", "TRACE"], logLevel => { + _.each(["Debug", "Release"], configuration => { + it(`should return correct args when workspace is ${hasProjectWorkspace} with ${logLevel} log level and ${configuration} configuration`, async () => { + const injector = createTestInjector({ logLevel, hasProjectWorkspace }); + + const buildConfig = { buildForDevice: false, release: configuration === "Release" }; + const xcodebuildArgsService = injector.resolve("xcodebuildArgsService"); + const actualArgs = await xcodebuildArgsService.getBuildForSimulatorArgs({ projectRoot }, { projectName }, buildConfig); + + const expectedArgs = [ + "ONLY_ACTIVE_ARCH=NO", + "CODE_SIGN_IDENTITY=", + "build", + "-configuration", configuration, + "-sdk", "iphonesimulator" + ] + .concat(getCommonArgs()) + .concat(getBuildLoggingArgs(logLevel)) + .concat(getXcodeProjectArgs({ hasProjectWorkspace, hasTarget: true })); + + assert.deepEqual(actualArgs, expectedArgs); + }); + }); + }); + }); + }); + describe("getBuildForDeviceArgs", () => { + const testCases = [ + { + name: "should return correct args when there are more than one connected device", + connectedDevices: [{deviceInfo: {activeArchitecture: "arm64"}}, {deviceInfo: {activeArchitecture: "armv7"}}], + expectedArgs: ["ONLY_ACTIVE_ARCH=NO", "-sdk", "iphoneos"].concat(getCommonArgs()) + }, + { + name: "should return correct args when there is only one connected device", + connectedDevices: [{deviceInfo: {activeArchitecture: "arm64"}}], + expectedArgs: ["-sdk", "iphoneos"].concat(getCommonArgs()) + }, + { + name: "should return correct args when no connected devices", + connectedDevices: [], + expectedArgs: ["-sdk", "iphoneos"].concat(getCommonArgs()) + } + ]; + + _.each(testCases, testCase => { + _.each([true, false], hasProjectWorkspace => { + _.each(["INFO", "TRACE"], logLevel => { + _.each(["Debug", "Release"], configuration => { + it(`${testCase.name} when hasProjectWorkspace is ${hasProjectWorkspace} with ${logLevel} log level and ${configuration} configuration`, async () => { + const injector = createTestInjector({ logLevel, hasProjectWorkspace, connectedDevices: testCase.connectedDevices }); + + const platformData = { projectRoot, getBuildOutputPath: () => buildOutputPath }; + const projectData = { projectName }; + const buildConfig = { buildForDevice: true, release: configuration === "Release" }; + const xcodebuildArgsService: IXcodebuildArgsService = injector.resolve("xcodebuildArgsService"); + const actualArgs = await xcodebuildArgsService.getBuildForDeviceArgs(platformData, projectData, buildConfig); + + const expectedArgs = [ + "archive", + "-archivePath", path.join(buildOutputPath, `${projectName}.xcarchive`), + "-configuration", configuration + ] + .concat(getXcodeProjectArgs({ hasProjectWorkspace })) + .concat(testCase.expectedArgs) + .concat(getBuildLoggingArgs(logLevel)); + + assert.deepEqual(actualArgs, expectedArgs); + }); + }); + }); + }); + }); + }); +}); diff --git a/test/services/ios/xcodebuild-service.ts b/test/services/ios/xcodebuild-service.ts new file mode 100644 index 0000000000..17f1dfebca --- /dev/null +++ b/test/services/ios/xcodebuild-service.ts @@ -0,0 +1,93 @@ +import { Yok } from "../../../lib/common/yok"; +import { XcodebuildService } from "../../../lib/services/ios/xcodebuild-service"; +import * as path from "path"; +import { assert } from "chai"; + +const projectRoot = "path/to/my/app/folder/platforms/ios"; +const projectName = "myApp"; +const buildOutputPath = path.join(projectRoot, projectName, "archive"); +const exportOptionsPlistOutput = { + exportFileDir: buildOutputPath, + exportFilePath: path.join(buildOutputPath, `${projectName}.ipa`), + exportOptionsPlistFilePath: "/my/temp/options/plist/file/path" +}; +let actualBuildArgs: string[] = []; +let actualBuildOptions:IXcodebuildCommandOptions = null; + +function createTestInjector(): IInjector { + const injector = new Yok(); + injector.register("exportOptionsPlistService", { + createDevelopmentExportOptionsPlist: () => exportOptionsPlistOutput, + createDistributionExportOptionsPlist: () => exportOptionsPlistOutput + }); + injector.register("xcodebuildArgsService", { + getBuildForDeviceArgs: async () => [], + getBuildForSimulatorArgs: async () => [] + }); + injector.register("xcodebuildCommandService", { + executeCommand: async (args: string[], options: IXcodebuildCommandOptions) => { + actualBuildArgs = args; + actualBuildOptions = options; + } + }); + + injector.register("xcodebuildService", XcodebuildService); + + return injector; +} + +describe("xcodebuildService", () => { + describe("buildForDevice", () => { + it("should build correctly for device", async () => { + const injector = createTestInjector(); + const xcodebuildService = injector.resolve("xcodebuildService"); + const platformData = { getBuildOutputPath: () => buildOutputPath, projectRoot }; + const projectData = { projectName }; + + const buildResult = await xcodebuildService.buildForDevice(platformData, projectData, { }); + + const expectedBuildArgs = [ + '-exportArchive', + '-archivePath', path.join(platformData.getBuildOutputPath(), `${projectName}.xcarchive`), + '-exportPath', exportOptionsPlistOutput.exportFileDir, + '-exportOptionsPlist', exportOptionsPlistOutput.exportOptionsPlistFilePath + ]; + assert.deepEqual(actualBuildArgs, expectedBuildArgs); + assert.deepEqual(actualBuildOptions, { cwd: projectRoot, stdio: undefined }); + assert.deepEqual(buildResult, exportOptionsPlistOutput.exportFilePath); + }); + }); + describe("buildForSimulator", () => { + it("should build correctly for simulator", async () => { + const injector = createTestInjector(); + const xcodebuildService = injector.resolve("xcodebuildService"); + const platformData = { getBuildOutputPath: () => buildOutputPath, projectRoot }; + const projectData = { projectName }; + + await xcodebuildService.buildForSimulator(platformData, projectData, {}); + + assert.deepEqual(actualBuildArgs, []); + assert.deepEqual(actualBuildOptions, { cwd: projectRoot, stdio: undefined }); + }); + }); + describe("buildForAppStore", () => { + it("should build correctly for Appstore", async () => { + const injector = createTestInjector(); + const xcodebuildService = injector.resolve("xcodebuildService"); + const platformData = { getBuildOutputPath: () => buildOutputPath, projectRoot }; + const projectData = { projectName }; + + const buildResult = await xcodebuildService.buildForAppStore(platformData, projectData, {}); + + const expectedBuildArgs = [ + '-exportArchive', + '-archivePath', path.join(platformData.getBuildOutputPath(), `${projectName}.xcarchive`), + '-exportPath', exportOptionsPlistOutput.exportFileDir, + '-exportOptionsPlist', exportOptionsPlistOutput.exportOptionsPlistFilePath + ]; + assert.deepEqual(actualBuildArgs, expectedBuildArgs); + assert.deepEqual(actualBuildOptions, { cwd: projectRoot }); + assert.deepEqual(buildResult, exportOptionsPlistOutput.exportFilePath); + }); + }); +}); 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 8eab7ee01e..a93d4a7562 100644 --- a/test/services/playground/preview-app-livesync-service.ts +++ b/test/services/playground/preview-app-livesync-service.ts @@ -1,13 +1,19 @@ import { Yok } from "../../../lib/common/yok"; import * as _ from 'lodash'; -import { LoggerStub, ErrorsStub, WorkflowServiceStub } from "../../stubs"; +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 { 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 +36,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,17 +102,25 @@ 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[] }) { options = options || {}; const injector = new Yok(); - injector.register("workflowService", WorkflowServiceStub); 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 @@ -126,7 +141,11 @@ function createTestInjector(options?: { getExternalPlugins: () => [] }); injector.register("projectFilesManager", ProjectFilesManager); - injector.register("previewAppLiveSyncService", PreviewAppLiveSyncService); + 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); @@ -150,18 +169,21 @@ 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] }); injector.register("previewAppFilesService", PreviewAppFilesService); + injector.register("previewQrCodeService", { + getQrCodeUrl: () => ({}), + getLiveSyncQrCode: () => ({}) + }); injector.register("analyticsService", { trackEventActionInGoogleAnalytics: () => ({}) }); + injector.register("hooksService", { + executeBeforeHooks: () => ({}), + executeAfterHooks: () => ({}) + }); return injector; } @@ -170,22 +192,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.startPreview(syncFilesData); if (actOptions.callGetInitialFiles) { await previewSdkService.getInitialFiles(deviceMockData); } @@ -194,23 +218,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.startPreview(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) { @@ -224,7 +248,7 @@ async function assert(expectedFiles: string[], options?: IAssertOptions) { function reset() { isComparePluginsOnDeviceCalled = false; - isHookCalledWithHMR = false; + isHMRPassedToEnv = false; applyChangesParams = []; initialFiles = []; readTextParams = []; @@ -239,7 +263,7 @@ function mapFiles(files: string[]): FilePayload[] { return files.map(file => { return { event: "change", - file: path.join("..", "platforms", "app", file), + file, fileContents: undefined, binary: false }; @@ -268,14 +292,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()); @@ -296,29 +320,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", @@ -357,19 +358,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/project-data-service.ts b/test/services/project-data-service.ts index 72f1db2b95..98c304e431 100644 --- a/test/services/project-data-service.ts +++ b/test/services/project-data-service.ts @@ -1,7 +1,7 @@ import { Yok } from "../../lib/common/yok"; import { assert } from "chai"; import { ProjectDataService } from "../../lib/services/project-data-service"; -import { LoggerStub, WorkflowServiceStub, ProjectDataStub } from "../stubs"; +import { LoggerStub, ProjectDataStub } from "../stubs"; import { NATIVESCRIPT_PROPS_INTERNAL_DELIMITER, PACKAGE_JSON_FILE_NAME, AssetConstants, ProjectTypes } from '../../lib/constants'; import { DevicePlatformsConstants } from "../../lib/common/mobile/device-platforms-constants"; import { basename, join } from "path"; @@ -43,7 +43,6 @@ const testData: any = [ const createTestInjector = (readTextData?: string): IInjector => { const testInjector = new Yok(); - testInjector.register("workflowService", WorkflowServiceStub); testInjector.register("projectData", ProjectDataStub); testInjector.register("staticConfig", { CLIENT_NAME_KEY_IN_PROJECT_FILE: CLIENT_NAME_KEY_IN_PROJECT_FILE, 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 3e2fee32b2..353a0162d4 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 { initialize(opts?: ILoggerOptions): void { } @@ -40,6 +41,8 @@ export class LoggerStub implements ILogger { printInfoMessageOnSameLine(message: string): void { } async printMsgWithTimeout(message: string, timeout: number): Promise { } printOnStderr(formatStr?: any, ...args: any[]): void { } + + isVerbose(): boolean { return false; } } export class FileSystemStub implements IFileSystem { @@ -295,13 +298,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; @@ -309,7 +315,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; @@ -361,13 +367,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: "", @@ -375,13 +386,7 @@ export class PlatformProjectServiceStub extends EventEmitter implements IPlatfor fastLivesyncFileExtensions: [] }; } - prebuildNativePlugin(options: IPluginBuildOptions): Promise { - return Promise.resolve(); - } - checkIfPluginsNeedBuild(projectData: IProjectData): Promise> { - return Promise.resolve([]); - } getAppResourcesDestinationDirectoryPath(): string { return ""; } @@ -391,9 +396,6 @@ export class PlatformProjectServiceStub extends EventEmitter implements IPlatfor validate(): Promise { return Promise.resolve({}); } - validatePlugins(projectData: IProjectData) { - return Promise.resolve(); - } async createProject(projectRoot: string, frameworkDir: string): Promise { return Promise.resolve(); } @@ -425,7 +427,7 @@ export class PlatformProjectServiceStub extends EventEmitter implements IPlatfor async updatePlatform(currentVersion: string, newVersion: string, canUpdate: boolean): Promise { return Promise.resolve(true); } - prepareAppResources(appResourcesDirectoryPath: string): void { } + prepareAppResources(projectData: IProjectData): void { } async preparePluginNativeCode(pluginData: IPluginData): Promise { return Promise.resolve(); @@ -451,10 +453,10 @@ export class PlatformProjectServiceStub extends EventEmitter implements IPlatfor async stopServices(): Promise { return Promise.resolve({ stderr: "", stdout: "", exitCode: 0 }); } - async cleanProject(projectRoot: string, projectData: IProjectData): Promise { + async cleanProject(projectRoot: string): 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 { @@ -468,15 +470,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: [] }), @@ -492,9 +495,6 @@ export class PlatformsDataStub extends EventEmitter implements IPlatformsData { } export class ProjectDataService implements IProjectDataService { - setUseLegacyWorkflow(projectDir: string, value: any): Promise { - return; - } getNSValue(propertyName: string): any { return {}; } @@ -505,7 +505,12 @@ export class ProjectDataService implements IProjectDataService { removeDependency(dependencyName: string): void { } - getProjectData(projectDir: string): IProjectData { return null; } + getProjectData(projectDir: string): IProjectData { + const projectData = new ProjectDataStub(); + projectData.initializeProjectData(projectDir); + + return projectData; + } async getAssetsStructure(opts: IProjectDir): Promise { return null; @@ -658,7 +663,7 @@ export class LiveSyncServiceStub extends EventEmitter implements ILiveSyncServic return; } - public async liveSync(deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise { + public async liveSync(deviceDescriptors: ILiveSyncDeviceDescriptor[], liveSyncData: ILiveSyncInfo): Promise { return; } @@ -666,7 +671,7 @@ export class LiveSyncServiceStub extends EventEmitter implements ILiveSyncServic return; } - public getLiveSyncDeviceDescriptors(projectDir: string): ILiveSyncDeviceInfo[] { + public getLiveSyncDeviceDescriptors(projectDir: string): ILiveSyncDeviceDescriptor[] { return []; } } @@ -736,18 +741,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 async savePrepareInfo(platformData: IPlatformData, projectData: IProjectData, prepareData: IPrepareData): Promise { } - public getPrepareInfoFilePath(platform: string): string { + public getPrepareInfoFilePath(platformData: IPlatformData): string { return ""; } @@ -755,7 +760,7 @@ export class ProjectChangesService implements IProjectChangesService { return {}; } - public setNativePlatformStatus(platform: string, projectData: IProjectData, nativePlatformStatus: IAddedNativePlatform): void { + public async setNativePlatformStatus(platformData: IPlatformData, projectData: IProjectData, addedPlatform: IAddedNativePlatform): Promise { return; } } @@ -779,119 +784,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; @@ -919,9 +811,32 @@ export class PerformanceService implements IPerformanceService { processExecutionData() { } } -export class WorkflowServiceStub implements IWorkflowService { - handleLegacyWorkflow(options: IHandleLegacyWorkflowOptions): Promise { - return; +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; } } @@ -940,13 +855,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); @@ -963,6 +877,6 @@ export class InjectorStub extends Yok implements IInjector { getDevice: (): Mobile.IDevice => undefined, getDeviceByIdentifier: (): Mobile.IDevice => undefined }); - this.register("workflowService", WorkflowServiceStub); + this.register("terminalSpinnerService", TerminalSpinnerServiceStub); } } diff --git a/test/tns-appstore-upload.ts b/test/tns-appstore-upload.ts index 970a4d7771..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,7 +18,10 @@ class AppStore { options: any; prompter: PrompterStub; projectData: ProjectDataStub; - platformService: any; + buildController: BuildController; + prepareNativePlatformService: PrepareNativePlatformService; + platformCommandHelper: any; + platformValidationService: any; iOSPlatformData: any; iOSProjectService: any; loggerService: LoggerStub; @@ -23,20 +29,14 @@ class AppStore { // Counters preparePlatformCalls: number = 0; - expectedPreparePlatformCalls: number = 0; archiveCalls: number = 0; expectedArchiveCalls: number = 0; exportArchiveCalls: number = 0; - expectedExportArchiveCalls: number = 0; itmsTransporterServiceUploadCalls: number = 0; expectedItmsTransporterServiceUploadCalls: number = 0; before() { this.iOSPlatformData = { - "platformProjectService": this.iOSProjectService = { - archive() { console.log("Archive!"); }, - exportArchive() { console.log("Export Archive!"); } - }, "projectRoot": "/Users/person/git/MyProject" }; this.initInjector({ @@ -56,15 +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; } - } + }, } }); + this.projectData.initializeProjectData(this.iOSPlatformData.projectRoot); this.command = this.injector.resolveCommand("appstore"); } @@ -79,13 +88,13 @@ 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.exportArchiveCalls, this.expectedExportArchiveCalls, "Mismatched number of iOSProjectService.exportArchive calls."); chai.assert.equal(this.itmsTransporterServiceUploadCalls, this.expectedItmsTransporterServiceUploadCalls, "Mismatched number of itmsTransporterService.upload calls."); } @@ -96,34 +105,12 @@ 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.iOSProjectService.archive = (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"); - }; - } - - expectExportArchive(expectedOptions?: { teamID?: string }) { - this.expectedExportArchiveCalls = 1; - this.iOSProjectService.exportArchive = (projectData: IProjectData, options?: { teamID?: string, archivePath?: string }) => { - this.exportArchiveCalls++; - chai.assert.equal(options.archivePath, "/Users/person/git/MyProject/platforms/ios/archive/MyProject.xcarchive", "Expected xcarchive path to be the one that we just archived."); - if (expectedOptions && expectedOptions.teamID) { - chai.assert.equal(options.teamID, expectedOptions.teamID, "Expected --team-id to be passed as teamID to the exportArchive"); - } else { - chai.assert.isUndefined(options.teamID, "Expected teamID in exportArchive to be undefined"); - } + 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"); }; } @@ -142,9 +129,7 @@ class AppStore { async noArgs() { this.expectItunesPrompt(); - this.expectPreparePlatform(); this.expectArchive(); - this.expectExportArchive(); this.expectITMSTransporterUpload(); await this.command.execute([]); @@ -153,9 +138,7 @@ class AppStore { } async itunesconnectArgs() { - this.expectPreparePlatform(); this.expectArchive(); - this.expectExportArchive(); this.expectITMSTransporterUpload(); await this.command.execute([AppStore.itunesconnect.user, AppStore.itunesconnect.pass]); @@ -165,9 +148,7 @@ class AppStore { async teamIdOption() { this.expectItunesPrompt(); - this.expectPreparePlatform(); this.expectArchive(); - this.expectExportArchive({ teamID: "MyTeamID" }); this.expectITMSTransporterUpload(); this.options.teamId = "MyTeamID"; diff --git a/test/update.ts b/test/update.ts index f6a2052017..33df3b302b 100644 --- a/test/update.ts +++ b/test/update.ts @@ -25,7 +25,6 @@ function createTestInjector( ): IInjector { const testInjector: IInjector = new yok.Yok(); testInjector.register("logger", stubs.LoggerStub); - testInjector.register("workflowService", stubs.WorkflowServiceStub); testInjector.register("options", Options); testInjector.register('fs', stubs.FileSystemStub); testInjector.register("analyticsService", { @@ -48,9 +47,8 @@ function createTestInjector( return "1.0.0"; } }); - testInjector.register("pluginVariablesService", {}); - testInjector.register("platformService", { - getInstalledPlatforms: function (): string[] { + testInjector.register("platformCommandHelper", { + getInstalledPlatforms: function(): string[] { return installedPlatforms; }, getAvailablePlatforms: function (): string[] { @@ -59,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" @@ -95,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"]); @@ -111,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([]); @@ -119,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"]); @@ -144,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 () => { @@ -162,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"); @@ -181,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 () => { @@ -195,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 () => { @@ -241,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 () => { @@ -256,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}`)); }); }); });