diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index ef4717e4eb..4beee8e2c0 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -105,6 +105,7 @@ $injector.require("devicePathProvider", "./device-path-provider"); $injector.requireCommand("platform|clean", "./commands/platform-clean"); $injector.requirePublicClass("liveSyncService", "./services/livesync/livesync-service"); +$injector.require("liveSyncCommandHelper", "./services/livesync/livesync-command-helper"); $injector.require("debugLiveSyncService", "./services/livesync/debug-livesync-service"); $injector.require("androidLiveSyncService", "./services/livesync/android-livesync-service"); $injector.require("iOSLiveSyncService", "./services/livesync/ios-livesync-service"); diff --git a/lib/commands/debug.ts b/lib/commands/debug.ts index 7c090d2eb3..5c1ff1b5a7 100644 --- a/lib/commands/debug.ts +++ b/lib/commands/debug.ts @@ -1,24 +1,25 @@ import { CONNECTED_STATUS } from "../common/constants"; import { isInteractive } from "../common/helpers"; +import { cache } from "../common/decorators"; import { DebugCommandErrors } from "../constants"; -export abstract class DebugPlatformCommand implements ICommand { +export class DebugPlatformCommand implements ICommand { public allowedParameters: ICommandParameter[] = []; - public platform: string; constructor(private debugService: IPlatformDebugService, - private $devicesService: Mobile.IDevicesService, - private $debugDataService: IDebugDataService, + private platform: string, + protected $devicesService: Mobile.IDevicesService, protected $platformService: IPlatformService, protected $projectData: IProjectData, protected $options: IOptions, protected $platformsData: IPlatformsData, protected $logger: ILogger, protected $errors: IErrors, + private $debugDataService: IDebugDataService, private $debugLiveSyncService: IDebugLiveSyncService, private $config: IConfiguration, - private $prompter: IPrompter) { - this.$projectData.initializeProjectData(); + private $prompter: IPrompter, + private $liveSyncCommandHelper: ILiveSyncCommandHelper) { } public async execute(args: string[]): Promise { @@ -36,41 +37,7 @@ export abstract class DebugPlatformCommand implements ICommand { const selectedDeviceForDebug = await this.getDeviceForDebug(); - const deviceDescriptors: ILiveSyncDeviceInfo[] = [selectedDeviceForDebug] - .map(d => { - const info: ILiveSyncDeviceInfo = { - identifier: d.deviceInfo.identifier, - buildAction: async (): Promise => { - const buildConfig: IBuildConfig = { - buildForDevice: !d.isEmulator, - 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, this.$projectData); - const pathToBuildResult = await this.$platformService.lastOutputPath(d.deviceInfo.platform, buildConfig, this.$projectData); - return pathToBuildResult; - } - }; - - return info; - }); - - const liveSyncInfo: ILiveSyncInfo = { - projectDir: this.$projectData.projectDir, - skipWatcher: !this.$options.watch || this.$options.justlaunch, - watchAllFiles: this.$options.syncAllFiles - }; - - await this.$debugLiveSyncService.liveSync(deviceDescriptors, liveSyncInfo); + await this.$liveSyncCommandHelper.getDevicesLiveSyncInfo([selectedDeviceForDebug], this.$debugLiveSyncService, this.platform); } public async getDeviceForDebug(): Promise { @@ -149,23 +116,25 @@ export abstract class DebugPlatformCommand implements ICommand { } } -export class DebugIOSCommand extends DebugPlatformCommand { +export class DebugIOSCommand implements ICommand { + + @cache() + private get debugPlatformCommand(): DebugPlatformCommand { + return this.$injector.resolve(DebugPlatformCommand, { debugService: this.$iOSDebugService, platform: this.platform }); + } + + public allowedParameters: ICommandParameter[] = []; + constructor(protected $errors: IErrors, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - $logger: ILogger, - $iOSDebugService: IPlatformDebugService, - $devicesService: Mobile.IDevicesService, - $config: IConfiguration, - $debugDataService: IDebugDataService, - $platformService: IPlatformService, - $options: IOptions, - $projectData: IProjectData, - $platformsData: IPlatformsData, - $iosDeviceOperations: IIOSDeviceOperations, - $debugLiveSyncService: IDebugLiveSyncService, - $prompter: IPrompter) { - super($iOSDebugService, $devicesService, $debugDataService, $platformService, $projectData, $options, $platformsData, $logger, - $errors, $debugLiveSyncService, $config, $prompter); + private $platformService: IPlatformService, + private $options: IOptions, + private $injector: IInjector, + private $projectData: IProjectData, + private $platformsData: IPlatformsData, + private $iOSDebugService: IDebugService, + $iosDeviceOperations: IIOSDeviceOperations) { + this.$projectData.initializeProjectData(); // Do not dispose ios-device-lib, so the process will remain alive and the debug application (NativeScript Inspector or Chrome DevTools) will be able to connect to the socket. // In case we dispose ios-device-lib, the socket will be closed and the code will fail when the debug application tries to read/send data to device socket. // That's why the `$ tns debug ios --justlaunch` command will not release the terminal. @@ -173,12 +142,16 @@ export class DebugIOSCommand extends DebugPlatformCommand { $iosDeviceOperations.setShouldDispose(false); } + public execute(args: string[]): Promise { + return this.debugPlatformCommand.execute(args); + } + public async canExecute(args: string[]): Promise { if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); } - return await super.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.iOS); + return await this.debugPlatformCommand.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.iOS); } public platform = this.$devicePlatformsConstants.iOS; @@ -186,26 +159,31 @@ export class DebugIOSCommand extends DebugPlatformCommand { $injector.registerCommand("debug|ios", DebugIOSCommand); -export class DebugAndroidCommand extends DebugPlatformCommand { +export class DebugAndroidCommand implements ICommand { + + @cache() + private get debugPlatformCommand(): DebugPlatformCommand { + return this.$injector.resolve(DebugPlatformCommand, { debugService: this.$androidDebugService, platform: this.platform }); + } + + public allowedParameters: ICommandParameter[] = []; + constructor(protected $errors: IErrors, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - $logger: ILogger, - $androidDebugService: IPlatformDebugService, - $devicesService: Mobile.IDevicesService, - $config: IConfiguration, - $debugDataService: IDebugDataService, - $platformService: IPlatformService, - $options: IOptions, - $projectData: IProjectData, - $platformsData: IPlatformsData, - $debugLiveSyncService: IDebugLiveSyncService, - $prompter: IPrompter) { - super($androidDebugService, $devicesService, $debugDataService, $platformService, $projectData, $options, $platformsData, $logger, - $errors, $debugLiveSyncService, $config, $prompter); + private $platformService: IPlatformService, + private $options: IOptions, + private $injector: IInjector, + private $projectData: IProjectData, + private $platformsData: IPlatformsData, + private $androidDebugService: IDebugService) { + this.$projectData.initializeProjectData(); } + public execute(args: string[]): Promise { + return this.debugPlatformCommand.execute(args); + } public async canExecute(args: string[]): Promise { - return await super.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.Android); + return await this.debugPlatformCommand.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.Android); } public platform = this.$devicePlatformsConstants.Android; diff --git a/lib/commands/run.ts b/lib/commands/run.ts index 548eacffb9..b966d73a3c 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -1,21 +1,20 @@ import { ERROR_NO_VALID_SUBCOMMAND_FORMAT } from "../common/constants"; +import { cache } from "../common/decorators"; export class RunCommandBase implements ICommand { - protected platform: string; + public platform: string; constructor(protected $platformService: IPlatformService, protected $liveSyncService: ILiveSyncService, protected $projectData: IProjectData, protected $options: IOptions, - protected $emulatorPlatformService: IEmulatorPlatformService, protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, protected $errors: IErrors, - private $devicesService: Mobile.IDevicesService, + protected $devicesService: Mobile.IDevicesService, + protected $platformsData: IPlatformsData, private $hostInfo: IHostInfo, - private $iosDeviceOperations: IIOSDeviceOperations, - private $mobileHelper: Mobile.IMobileHelper, - protected $platformsData: IPlatformsData) { - } + private $liveSyncCommandHelper: ILiveSyncCommandHelper + ) { } public allowedParameters: ICommandParameter[] = []; public async execute(args: string[]): Promise { @@ -28,6 +27,8 @@ export class RunCommandBase implements ICommand { } this.$projectData.initializeProjectData(); + this.platform = args[0] || this.platform; + if (!this.platform && !this.$hostInfo.isDarwin) { this.platform = this.$devicePlatformsConstants.Android; } @@ -56,93 +57,35 @@ export class RunCommandBase implements ICommand { }); await this.$devicesService.detectCurrentlyAttachedDevices(); - - const devices = this.$devicesService.getDeviceInstances(); - // Now let's take data for each device: - const deviceDescriptors: ILiveSyncDeviceInfo[] = devices.filter(d => !this.platform || d.deviceInfo.platform === this.platform) - .map(d => { - const info: ILiveSyncDeviceInfo = { - identifier: d.deviceInfo.identifier, - buildAction: async (): Promise => { - const buildConfig: IBuildConfig = { - buildForDevice: !d.isEmulator, - 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, this.$projectData); - const pathToBuildResult = await this.$platformService.lastOutputPath(d.deviceInfo.platform, buildConfig, this.$projectData); - return pathToBuildResult; - } - }; - - return info; - }); - - const workingWithiOSDevices = !this.platform || this.$mobileHelper.isiOSPlatform(this.platform); - const shouldKeepProcessAlive = this.$options.watch || !this.$options.justlaunch; - if (workingWithiOSDevices && shouldKeepProcessAlive) { - this.$iosDeviceOperations.setShouldDispose(false); - } - - if (this.$options.release || this.$options.bundle) { - const runPlatformOptions: IRunPlatformOptions = { - device: this.$options.device, - emulator: this.$options.emulator, - justlaunch: this.$options.justlaunch - }; - - const deployOptions = _.merge({ - projectDir: this.$projectData.projectDir, - clean: true, - }, this.$options.argv); - - await this.$platformService.deployPlatform(args[0], this.$options, deployOptions, this.$projectData, this.$options); - await this.$platformService.startApplication(args[0], runPlatformOptions, this.$projectData.projectId); - return this.$platformService.trackProjectType(this.$projectData); - } - - const liveSyncInfo: ILiveSyncInfo = { - projectDir: this.$projectData.projectDir, - skipWatcher: !this.$options.watch, - watchAllFiles: this.$options.syncAllFiles, - clean: this.$options.clean - }; - - await this.$liveSyncService.liveSync(deviceDescriptors, liveSyncInfo); + let devices = this.$devicesService.getDeviceInstances(); + devices = devices.filter(d => !this.platform || d.deviceInfo.platform.toLowerCase() === this.platform.toLowerCase()); + await this.$liveSyncCommandHelper.getDevicesLiveSyncInfo(devices, this.$liveSyncService, this.platform); } } $injector.registerCommand("run|*all", RunCommandBase); -export class RunIosCommand extends RunCommandBase implements ICommand { +export class RunIosCommand implements ICommand { + + @cache() + private get runCommand(): RunCommandBase { + const runCommand = this.$injector.resolve(RunCommandBase); + runCommand.platform = this.platform; + return runCommand; + } + public allowedParameters: ICommandParameter[] = []; public get platform(): string { return this.$devicePlatformsConstants.iOS; } - constructor($platformService: IPlatformService, - protected $platformsData: IPlatformsData, + constructor(protected $platformsData: IPlatformsData, protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, protected $errors: IErrors, - $liveSyncService: ILiveSyncService, - $projectData: IProjectData, - $options: IOptions, - $emulatorPlatformService: IEmulatorPlatformService, - $devicesService: Mobile.IDevicesService, - $hostInfo: IHostInfo, - $iosDeviceOperations: IIOSDeviceOperations, - $mobileHelper: Mobile.IMobileHelper) { - super($platformService, $liveSyncService, $projectData, $options, $emulatorPlatformService, $devicePlatformsConstants, $errors, - $devicesService, $hostInfo, $iosDeviceOperations, $mobileHelper, $platformsData); + private $injector: IInjector, + private $platformService: IPlatformService, + private $projectData: IProjectData, + private $options: IOptions) { } public async execute(args: string[]): Promise { @@ -150,44 +93,46 @@ export class RunIosCommand extends RunCommandBase implements ICommand { this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); } - return this.executeCore([this.$platformsData.availablePlatforms.iOS]); + return this.runCommand.executeCore(args); } public async canExecute(args: string[]): Promise { - return await super.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.iOS); + return await this.runCommand.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.iOS); } } $injector.registerCommand("run|ios", RunIosCommand); -export class RunAndroidCommand extends RunCommandBase implements ICommand { +export class RunAndroidCommand implements ICommand { + + @cache() + private get runCommand(): RunCommandBase { + const runCommand = this.$injector.resolve(RunCommandBase); + runCommand.platform = this.platform; + return runCommand; + } + public allowedParameters: ICommandParameter[] = []; public get platform(): string { return this.$devicePlatformsConstants.Android; } - constructor($platformService: IPlatformService, - protected $platformsData: IPlatformsData, + constructor(protected $platformsData: IPlatformsData, protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, protected $errors: IErrors, - $liveSyncService: ILiveSyncService, - $projectData: IProjectData, - $options: IOptions, - $emulatorPlatformService: IEmulatorPlatformService, - $devicesService: Mobile.IDevicesService, - $hostInfo: IHostInfo, - $iosDeviceOperations: IIOSDeviceOperations, - $mobileHelper: Mobile.IMobileHelper) { - super($platformService, $liveSyncService, $projectData, $options, $emulatorPlatformService, $devicePlatformsConstants, $errors, - $devicesService, $hostInfo, $iosDeviceOperations, $mobileHelper, $platformsData); + private $injector: IInjector, + private $platformService: IPlatformService, + private $projectData: IProjectData, + private $options: IOptions) { } public async execute(args: string[]): Promise { - return this.executeCore([this.$platformsData.availablePlatforms.Android]); + return this.runCommand.executeCore(args); } public async canExecute(args: string[]): Promise { - await super.canExecute(args); + await this.runCommand.canExecute(args); + if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.Android, this.$projectData)) { this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.Android} can not be built on this OS`); } diff --git a/lib/common b/lib/common index d9491f33cb..c33a233679 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit d9491f33cbdb991caca18a94f5dc2a97eba4111b +Subproject commit c33a23367970966d1993749886fd689f3d048213 diff --git a/lib/constants.ts b/lib/constants.ts index d58e856ae4..d7512a2f78 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -92,3 +92,9 @@ 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."; } + +export const enum NativePlatformStatus { + requiresPlatformAdd = "1", + requiresPrepare = "2", + alreadyPrepared = "3" +} diff --git a/lib/definitions/livesync.d.ts b/lib/definitions/livesync.d.ts index 472c76cc50..d420e15a64 100644 --- a/lib/definitions/livesync.d.ts +++ b/lib/definitions/livesync.d.ts @@ -120,6 +120,11 @@ interface ILiveSyncInfo { * Forces a build before the initial livesync. */ clean?: boolean; + + /** + * Whether to skip preparing the native platform. + */ + skipNativePrepare?: boolean; } interface ILatestAppPackageInstalledSettings extends IDictionary> { /* empty */ } @@ -249,3 +254,7 @@ interface IDevicePathProvider { getDeviceProjectRootPath(device: Mobile.IDevice, options: IDeviceProjectRootOptions): Promise; getDeviceSyncZipPath(device: Mobile.IDevice): string; } + +interface ILiveSyncCommandHelper { + getDevicesLiveSyncInfo(devices: Mobile.IDevice[], liveSyncService: ILiveSyncService, platform: string): Promise; +} diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts index 57450d94f7..5645e49f21 100644 --- a/lib/definitions/platform.d.ts +++ b/lib/definitions/platform.d.ts @@ -47,7 +47,7 @@ interface IPlatformService extends NodeJS.EventEmitter { * @param {Array} filesToSync Files about to be synced to device. * @returns {boolean} true indicates that the platform was prepared. */ - preparePlatform(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformTemplate: string, projectData: IProjectData, config: IAddPlatformCoreOptions, filesToSync?: Array): Promise; + preparePlatform(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformTemplate: string, projectData: IProjectData, config: IAddPlatformCoreOptions, filesToSync?: Array, nativePrepare?: INativePrepare): Promise; /** * Determines whether a build is necessary. A build is necessary when one of the following is true: @@ -288,6 +288,7 @@ interface IPlatformsData { interface INodeModulesBuilder { prepareNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime: Date, projectData: IProjectData): Promise; + prepareJSNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime: Date, projectData: IProjectData): Promise; cleanNodeModules(absoluteOutputPath: string, platform: string): void; } diff --git a/lib/definitions/plugins.d.ts b/lib/definitions/plugins.d.ts index 24926cd12a..7feb3d5bc6 100644 --- a/lib/definitions/plugins.d.ts +++ b/lib/definitions/plugins.d.ts @@ -4,6 +4,7 @@ interface IPluginsService { prepare(pluginData: IDependencyData, platform: string, projectData: IProjectData): Promise; getAllInstalledPlugins(projectData: IProjectData): Promise; ensureAllDependenciesAreInstalled(projectData: IProjectData): Promise; + preparePluginScripts(pluginData: IPluginData, platform: string, projectData: IProjectData): void /** * Returns all dependencies and devDependencies from pacakge.json file. diff --git a/lib/definitions/project-changes.d.ts b/lib/definitions/project-changes.d.ts index ceefe2b371..04ae5edab4 100644 --- a/lib/definitions/project-changes.d.ts +++ b/lib/definitions/project-changes.d.ts @@ -1,14 +1,14 @@ -interface IPrepareInfo { +interface IPrepareInfo extends IAddedNativePlatform { time: string; bundle: boolean; release: boolean; + projectFileHash: string; changesRequireBuild: boolean; changesRequireBuildTime: string; - iOSProvisioningProfileUUID?: string; } -interface IProjectChangesInfo { +interface IProjectChangesInfo extends IAddedNativePlatform { appFilesChanged: boolean; appResourcesChanged: boolean; modulesChanged: boolean; @@ -22,12 +22,22 @@ interface IProjectChangesInfo { readonly changesRequirePrepare: boolean; } -interface IProjectChangesOptions extends IAppFilesUpdaterOptions, IProvision {} +interface IProjectChangesOptions extends IAppFilesUpdaterOptions, IProvision { + nativePlatformStatus?: "1" | "2" | "3"; +} interface IProjectChangesService { checkForChanges(platform: string, projectData: IProjectData, buildOptions: IProjectChangesOptions): IProjectChangesInfo; 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; -} \ No newline at end of file +} + +/** + * NativePlatformStatus.requiresPlatformAdd | NativePlatformStatus.requiresPrepare | NativePlatformStatus.alreadyPrepared + */ +interface IAddedNativePlatform { + nativePlatformStatus: "1" | "2" | "3"; +} diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index a0aa63b25a..2656ddf251 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -132,6 +132,10 @@ interface IBuildForDevice { buildForDevice: boolean; } +interface INativePrepare { + skipNativePrepare: boolean; +} + interface IBuildConfig extends IAndroidBuildOptionsSettings, IiOSBuildConfig { projectDir: string; clean?: boolean; diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index 170bae39d9..4dabcefe6d 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -221,7 +221,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject let manifestPath = this.getPlatformData(projectData).configurationFilePath; shell.sed('-i', /__PACKAGE__/, projectData.projectId, manifestPath); if (this.$androidToolsInfo.getToolsInfo().androidHomeEnvVar) { - const sdk = (platformSpecificData && platformSpecificData.sdk) || (this.$androidToolsInfo.getToolsInfo().compileSdkVersion).toString(); + const sdk = (platformSpecificData && platformSpecificData.sdk) || (this.$androidToolsInfo.getToolsInfo().compileSdkVersion || "").toString(); shell.sed('-i', /__APILEVEL__/, sdk, manifestPath); } } diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index b684faec92..db60e0f23f 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -876,7 +876,6 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f if (!this.$fs.exists(xcuserDataPath) && !this.$fs.exists(sharedDataPath)) { this.$logger.info("Creating project scheme..."); - await this.checkIfXcodeprojIsRequired(); let createSchemeRubyScript = `ruby -e "require 'xcodeproj'; xcproj = Xcodeproj::Project.open('${projectData.projectName}.xcodeproj'); xcproj.recreate_user_schemes; xcproj.save"`; @@ -1123,13 +1122,11 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f this.$fs.writeFile(projectFile, ""); } - if (this.$hostInfo.isDarwin) { - await this.checkIfXcodeprojIsRequired(); - let escapedProjectFile = projectFile.replace(/'/g, "\\'"), - escapedPluginFile = pluginFile.replace(/'/g, "\\'"), - mergeScript = `require 'xcodeproj'; Xcodeproj::Config.new('${escapedProjectFile}').merge(Xcodeproj::Config.new('${escapedPluginFile}')).save_as(Pathname.new('${escapedProjectFile}'))`; - await this.$childProcess.exec(`ruby -e "${mergeScript}"`); - } + await this.checkIfXcodeprojIsRequired(); + let escapedProjectFile = projectFile.replace(/'/g, "\\'"), + escapedPluginFile = pluginFile.replace(/'/g, "\\'"), + mergeScript = `require 'xcodeproj'; Xcodeproj::Config.new('${escapedProjectFile}').merge(Xcodeproj::Config.new('${escapedPluginFile}')).save_as(Pathname.new('${escapedProjectFile}'))`; + await this.$childProcess.exec(`ruby -e "${mergeScript}"`); } private async mergeProjectXcconfigFiles(release: boolean, projectData: IProjectData): Promise { diff --git a/lib/services/livesync/livesync-command-helper.ts b/lib/services/livesync/livesync-command-helper.ts new file mode 100644 index 0000000000..e37dd20b5f --- /dev/null +++ b/lib/services/livesync/livesync-command-helper.ts @@ -0,0 +1,87 @@ +export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { + + constructor(protected $platformService: IPlatformService, + protected $projectData: IProjectData, + protected $options: IOptions, + protected $devicesService: Mobile.IDevicesService, + private $iosDeviceOperations: IIOSDeviceOperations, + private $mobileHelper: Mobile.IMobileHelper, + private $platformsData: IPlatformsData) { + } + + public async getDevicesLiveSyncInfo(devices: Mobile.IDevice[], liveSyncService: ILiveSyncService, platform: string): Promise { + await this.$devicesService.detectCurrentlyAttachedDevices(); + + const workingWithiOSDevices = !platform || this.$mobileHelper.isiOSPlatform(platform); + const shouldKeepProcessAlive = this.$options.watch || !this.$options.justlaunch; + if (workingWithiOSDevices && shouldKeepProcessAlive) { + this.$iosDeviceOperations.setShouldDispose(false); + } + + if (this.$options.release || this.$options.bundle) { + await this.runInReleaseMode(platform); + return; + } + + // Now let's take data for each device: + const deviceDescriptors: ILiveSyncDeviceInfo[] = devices + .map(d => { + const info: ILiveSyncDeviceInfo = { + identifier: d.deviceInfo.identifier, + buildAction: async (): Promise => { + const buildConfig: IBuildConfig = { + buildForDevice: !d.isEmulator, + 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, this.$projectData); + const result = await this.$platformService.lastOutputPath(d.deviceInfo.platform, buildConfig, this.$projectData); + return result; + } + }; + + return info; + }); + + const liveSyncInfo: ILiveSyncInfo = { + projectDir: this.$projectData.projectDir, + skipWatcher: !this.$options.watch, + watchAllFiles: this.$options.syncAllFiles, + clean: this.$options.clean + }; + + await liveSyncService.liveSync(deviceDescriptors, liveSyncInfo); + + } + + private async runInReleaseMode(platform: string): Promise { + const runPlatformOptions: IRunPlatformOptions = { + device: this.$options.device, + emulator: this.$options.emulator, + justlaunch: this.$options.justlaunch + }; + + const deployOptions = _.merge({ + projectDir: this.$projectData.projectDir, + clean: true, + }, this.$options.argv); + + const availablePlatforms = platform ? [platform] : _.values(this.$platformsData.availablePlatforms); + for (const currentPlatform of availablePlatforms) { + await this.$platformService.deployPlatform(currentPlatform, this.$options, deployOptions, this.$projectData, this.$options); + await this.$platformService.startApplication(currentPlatform, runPlatformOptions, this.$projectData.projectId); + this.$platformService.trackProjectType(this.$projectData); + } + } +} + +$injector.register("liveSyncCommandHelper", LiveSyncCommandHelper); diff --git a/lib/services/livesync/livesync-service.ts b/lib/services/livesync/livesync-service.ts index 4e2b289977..dae7ecf36a 100644 --- a/lib/services/livesync/livesync-service.ts +++ b/lib/services/livesync/livesync-service.ts @@ -154,7 +154,7 @@ export class LiveSyncService extends EventEmitter implements ILiveSyncService { throw new Error(`Invalid platform ${platform}. Supported platforms are: ${this.$mobileHelper.platformNames.join(", ")}`); } - private async ensureLatestAppPackageIsInstalledOnDevice(options: IEnsureLatestAppPackageIsInstalledOnDeviceOptions): Promise { + private async ensureLatestAppPackageIsInstalledOnDevice(options: IEnsureLatestAppPackageIsInstalledOnDeviceOptions, nativePrepare?: INativePrepare): Promise { const platform = options.device.deviceInfo.platform; if (options.preparedPlatforms.indexOf(platform) === -1) { options.preparedPlatforms.push(platform); @@ -162,15 +162,11 @@ export class LiveSyncService extends EventEmitter implements ILiveSyncService { await this.$platformService.preparePlatform(platform, { bundle: false, release: false, - }, null, options.projectData, {}, options.modifiedFiles); + }, null, options.projectData, {}, options.modifiedFiles, nativePrepare); } - const rebuildInfo = _.find(options.rebuiltInformation, info => info.isEmulator === options.device.isEmulator && info.platform === platform); - - 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. - await this.$platformService.installApplication(options.device, { release: false }, options.projectData, rebuildInfo.pathToBuildItem, options.deviceBuildInfoDescriptor.outputPath); + const buildResult = await this.installedCachedAppPackage(platform, options); + if (buildResult) { return; } @@ -185,6 +181,15 @@ export class LiveSyncService extends EventEmitter implements ILiveSyncService { action = LiveSyncTrackActionNames.LIVESYNC_OPERATION_BUILD; } + await this.trackAction(action, platform, options); + + const shouldInstall = await this.$platformService.shouldInstall(options.device, options.projectData, options.deviceBuildInfoDescriptor.outputPath); + if (shouldInstall) { + await this.$platformService.installApplication(options.device, { release: false }, options.projectData, pathToBuildItem, options.deviceBuildInfoDescriptor.outputPath); + } + } + + private async trackAction(action: string, platform: string, options: IEnsureLatestAppPackageIsInstalledOnDeviceOptions): Promise { if (!options.settings[platform][options.device.deviceInfo.type]) { let isForDevice = !options.device.isEmulator; options.settings[platform][options.device.deviceInfo.type] = true; @@ -198,11 +203,19 @@ export class LiveSyncService extends EventEmitter implements ILiveSyncService { } await this.$platformService.trackActionForPlatform({ action: LiveSyncTrackActionNames.DEVICE_INFO, platform, isForDevice: !options.device.isEmulator, deviceOsVersion: options.device.deviceInfo.version }); + } - const shouldInstall = await this.$platformService.shouldInstall(options.device, options.projectData, options.deviceBuildInfoDescriptor.outputPath); - if (shouldInstall) { - await this.$platformService.installApplication(options.device, { release: false }, options.projectData, pathToBuildItem, options.deviceBuildInfoDescriptor.outputPath); + private async installedCachedAppPackage(platform: string, options: IEnsureLatestAppPackageIsInstalledOnDeviceOptions): Promise { + const rebuildInfo = _.find(options.rebuiltInformation, info => info.isEmulator === options.device.isEmulator && info.platform === platform); + + 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. + await this.$platformService.installApplication(options.device, { release: false }, options.projectData, rebuildInfo.pathToBuildItem, options.deviceBuildInfoDescriptor.outputPath); + return rebuildInfo.pathToBuildItem; } + + return null; } private async initialSync(projectData: IProjectData, deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise { @@ -224,7 +237,7 @@ export class LiveSyncService extends EventEmitter implements ILiveSyncService { deviceBuildInfoDescriptor, liveSyncData, settings - }); + }, { skipNativePrepare: liveSyncData.skipNativePrepare }); const liveSyncResultInfo = await this.getLiveSyncService(platform).fullSync({ projectData, device, @@ -326,7 +339,7 @@ export class LiveSyncService extends EventEmitter implements ILiveSyncService { deviceBuildInfoDescriptor, settings: latestAppPackageInstalledSettings, modifiedFiles: allModifiedFiles - }); + }, { skipNativePrepare: liveSyncData.skipNativePrepare }); const service = this.getLiveSyncService(device.deviceInfo.platform); const settings: ILiveSyncWatchInfo = { diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index fb68a435b6..76e3bee56f 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -39,8 +39,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { private $npm: INodePackageManager, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $projectChangesService: IProjectChangesService, - private $analyticsService: IAnalyticsService, - private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder) { + private $analyticsService: IAnalyticsService) { super(); } @@ -59,10 +58,17 @@ export class PlatformService extends EventEmitter implements IPlatformService { } public async addPlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, config: IAddPlatformCoreOptions, frameworkPath?: string): Promise { - let platformsDir = projectData.platformsDir; + const platformsDir = projectData.platformsDir; this.$fs.ensureDirectoryExists(platformsDir); for (let platform of platforms) { + this.validatePlatform(platform, projectData); + const platformPath = path.join(projectData.platformsDir, platform); + + if (this.$fs.exists(platformPath)) { + this.$errors.failWithoutHelp(`Platform ${platform} already added`); + } + await this.addPlatform(platform.toLowerCase(), platformTemplate, projectData, config, frameworkPath); } } @@ -78,19 +84,11 @@ export class PlatformService extends EventEmitter implements IPlatformService { return version; } - private async addPlatform(platformParam: string, platformTemplate: string, projectData: IProjectData, config: IAddPlatformCoreOptions, frameworkPath?: string): Promise { + private async addPlatform(platformParam: string, platformTemplate: string, projectData: IProjectData, config: IAddPlatformCoreOptions, frameworkPath?: string, nativePrepare?: INativePrepare): Promise { let data = platformParam.split("@"), platform = data[0].toLowerCase(), version = data[1]; - this.validatePlatform(platform, projectData); - - let platformPath = path.join(projectData.platformsDir, platform); - - if (this.$fs.exists(platformPath)) { - this.$errors.failWithoutHelp("Platform %s already added", platform); - } - let platformData = this.$platformsData.getPlatformData(platform, projectData); if (version === undefined) { @@ -116,15 +114,17 @@ export class PlatformService extends EventEmitter implements IPlatformService { npmOptions["version"] = version; } - let spinner = new clui.Spinner("Installing " + packageToInstall); - let projectDir = projectData.projectDir; + const spinner = new clui.Spinner("Installing " + packageToInstall); + const projectDir = projectData.projectDir; + const platformPath = path.join(projectData.platformsDir, platform); + try { spinner.start(); let downloadedPackagePath = await this.$npmInstallationManager.install(packageToInstall, projectDir, npmOptions); let frameworkDir = path.join(downloadedPackagePath, constants.PROJECT_FRAMEWORK_FOLDER_NAME); frameworkDir = path.resolve(frameworkDir); - let coreModuleName = await this.addPlatformCore(platformData, frameworkDir, platformTemplate, projectData, config); + const coreModuleName = await this.addPlatformCore(platformData, frameworkDir, platformTemplate, projectData, config, nativePrepare); await this.$npm.uninstall(coreModuleName, { save: true }, projectData.projectDir); } catch (err) { this.$fs.deleteDirectory(platformPath); @@ -137,17 +137,15 @@ export class PlatformService extends EventEmitter implements IPlatformService { } - private async addPlatformCore(platformData: IPlatformData, frameworkDir: string, platformTemplate: string, projectData: IProjectData, config: IAddPlatformCoreOptions): Promise { - let coreModuleData = this.$fs.readJson(path.join(frameworkDir, "..", "package.json")); - let installedVersion = coreModuleData.version; - let coreModuleName = coreModuleData.name; - - let customTemplateOptions = await this.getPathToPlatformTemplate(platformTemplate, platformData.frameworkPackageName, projectData.projectDir); + private async addPlatformCore(platformData: IPlatformData, frameworkDir: string, platformTemplate: string, projectData: IProjectData, config: IAddPlatformCoreOptions, nativePrepare?: INativePrepare): Promise { + const coreModuleData = this.$fs.readJson(path.join(frameworkDir, "..", "package.json")); + const installedVersion = coreModuleData.version; + const customTemplateOptions = await this.getPathToPlatformTemplate(platformTemplate, platformData.frameworkPackageName, projectData.projectDir); config.pathToTemplate = customTemplateOptions && customTemplateOptions.pathToTemplate; - await platformData.platformProjectService.createProject(path.resolve(frameworkDir), installedVersion, projectData, config); - platformData.platformProjectService.ensureConfigurationFileInAppResources(projectData); - await platformData.platformProjectService.interpolateData(projectData, config); - platformData.platformProjectService.afterCreateProject(platformData.projectRoot, projectData); + + if (!nativePrepare || !nativePrepare.skipNativePrepare) { + await this.addPlatformCoreNative(platformData, frameworkDir, installedVersion, projectData, config); + } let frameworkPackageNameData: any = { version: installedVersion }; if (customTemplateOptions) { @@ -156,10 +154,18 @@ export class PlatformService extends EventEmitter implements IPlatformService { this.$projectDataService.setNSValue(projectData.projectDir, platformData.frameworkPackageName, frameworkPackageNameData); + const coreModuleName = coreModuleData.name; return coreModuleName; } + private async addPlatformCoreNative(platformData: IPlatformData, frameworkDir: string, installedVersion: string, projectData: IProjectData, config: IAddPlatformCoreOptions): Promise { + await platformData.platformProjectService.createProject(path.resolve(frameworkDir), installedVersion, projectData, config); + platformData.platformProjectService.ensureConfigurationFileInAppResources(projectData); + await platformData.platformProjectService.interpolateData(projectData, config); + platformData.platformProjectService.afterCreateProject(platformData.projectRoot, projectData); + } + private async getPathToPlatformTemplate(selectedTemplate: string, frameworkPackageName: string, projectDir: string): Promise<{ selectedTemplate: string, pathToTemplate: string }> { if (!selectedTemplate) { // read data from package.json's nativescript key @@ -207,35 +213,13 @@ export class PlatformService extends EventEmitter implements IPlatformService { public getPreparedPlatforms(projectData: IProjectData): string[] { return _.filter(this.$platformsData.platformsNames, p => { return this.isPlatformPrepared(p, projectData); }); } + public async preparePlatform(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformTemplate: string, projectData: IProjectData, config: IAddPlatformCoreOptions, filesToSync?: Array, nativePrepare?: INativePrepare): Promise { + const platformData = this.$platformsData.getPlatformData(platform, projectData); + const changesInfo = await this.initialPrepare(platform, platformData, appFilesUpdaterOptions, platformTemplate, projectData, config, nativePrepare); + const requiresNativePrepare = (!nativePrepare || !nativePrepare.skipNativePrepare) && changesInfo.nativePlatformStatus === constants.NativePlatformStatus.requiresPrepare; - public async preparePlatform(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformTemplate: string, projectData: IProjectData, config: IAddPlatformCoreOptions, filesToSync?: Array): Promise { - this.validatePlatform(platform, projectData); - - await this.trackProjectType(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}`); - } - - let platformData = this.$platformsData.getPlatformData(platform, projectData); - await this.$pluginsService.validate(platformData, projectData); - - const bundle = appFilesUpdaterOptions.bundle; - - await this.ensurePlatformInstalled(platform, platformTemplate, projectData, config); - let changesInfo = this.$projectChangesService.checkForChanges(platform, projectData, { bundle, release: appFilesUpdaterOptions.release, provision: config.provision }); - - this.$logger.trace("Changes info in prepare platform:", changesInfo); - - if (changesInfo.hasChanges) { - await this.cleanProject(platform, appFilesUpdaterOptions, platformData, projectData); - } - if (changesInfo.hasChanges || bundle) { - await this.preparePlatformCore(platform, appFilesUpdaterOptions, projectData, config, changesInfo, filesToSync); + if (changesInfo.hasChanges || appFilesUpdaterOptions.bundle || requiresNativePrepare) { + await this.preparePlatformCore(platform, appFilesUpdaterOptions, projectData, config, changesInfo, filesToSync, nativePrepare); this.$projectChangesService.savePrepareInfo(platform, projectData); } else { this.$logger.out("Skipping prepare."); @@ -281,15 +265,56 @@ export class PlatformService extends EventEmitter implements IPlatformService { } } + private async initialPrepare(platform: string, platformData: IPlatformData, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformTemplate: string, projectData: IProjectData, config: IAddPlatformCoreOptions, nativePrepare?: INativePrepare): Promise { + this.validatePlatform(platform, projectData); + + await this.trackProjectType(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.$pluginsService.validate(platformData, projectData); + await this.ensurePlatformInstalled(platform, platformTemplate, projectData, config, nativePrepare); + + const bundle = appFilesUpdaterOptions.bundle; + const nativePlatformStatus = (nativePrepare && nativePrepare.skipNativePrepare) ? constants.NativePlatformStatus.requiresPlatformAdd : constants.NativePlatformStatus.requiresPrepare; + const changesInfo = this.$projectChangesService.checkForChanges(platform, projectData, { bundle, release: appFilesUpdaterOptions.release, provision: config.provision, nativePlatformStatus }); + + this.$logger.trace("Changes info in prepare platform:", changesInfo); + return changesInfo; + } + /* Hooks are expected to use "filesToSync" parameter, as to give plugin authors additional information about the sync process.*/ @helpers.hook('prepare') - private async preparePlatformCore(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, changesInfo?: IProjectChangesInfo, filesToSync?: Array): Promise { + private async preparePlatformCore(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, changesInfo?: IProjectChangesInfo, filesToSync?: Array, nativePrepare?: INativePrepare): Promise { this.$logger.out("Preparing project..."); let platformData = this.$platformsData.getPlatformData(platform, projectData); + await this.preparePlatformCoreJS(platform, platformData, appFilesUpdaterOptions, projectData, platformSpecificData, changesInfo); + + if (!nativePrepare || !nativePrepare.skipNativePrepare) { + await this.preparePlatformCoreNative(platform, platformData, appFilesUpdaterOptions, projectData, platformSpecificData, changesInfo); + } + let directoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); + let excludedDirs = [constants.APP_RESOURCES_FOLDER_NAME]; + if (!changesInfo || !changesInfo.modulesChanged) { + excludedDirs.push(constants.TNS_MODULES_FOLDER_NAME); + } + + this.$projectFilesManager.processPlatformSpecificFiles(directoryPath, platform, excludedDirs); + + this.$logger.out(`Project successfully prepared (${platform})`); + } + + private async preparePlatformCoreJS(platform: string, platformData: IPlatformData, appFilesUpdaterOptions: IAppFilesUpdaterOptions, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, changesInfo?: IProjectChangesInfo, filesToSync?: Array, ): Promise { if (!changesInfo || changesInfo.appFilesChanged) { - await this.copyAppFiles(platform, appFilesUpdaterOptions, projectData); + await this.copyAppFiles(platformData, appFilesUpdaterOptions, projectData); // remove the App_Resources folder from the app/assets as here we're applying other files changes. const appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); @@ -299,45 +324,41 @@ export class PlatformService extends EventEmitter implements IPlatformService { } } - if (!changesInfo || changesInfo.changesRequirePrepare) { - await this.copyAppFiles(platform, appFilesUpdaterOptions, projectData); - this.copyAppResources(platform, projectData); - await platformData.platformProjectService.prepareProject(projectData, platformSpecificData); + if (!changesInfo || changesInfo.modulesChanged) { + await this.copyTnsModules(platform, platformData, projectData); } + } - if (!changesInfo || changesInfo.modulesChanged) { - await this.copyTnsModules(platform, projectData); - } else if (appFilesUpdaterOptions.bundle) { - let dependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir); - for (let dependencyKey in dependencies) { - const dependency = dependencies[dependencyKey]; - let isPlugin = !!dependency.nativescript; - if (isPlugin) { - let pluginData = this.$pluginsService.convertToPluginData(dependency, projectData.projectDir); - await this.$pluginsService.preparePluginNativeCode(pluginData, platform, projectData); - } - } + public async preparePlatformCoreNative(platform: string, platformData: IPlatformData, appFilesUpdaterOptions: IAppFilesUpdaterOptions, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, changesInfo?: IProjectChangesInfo): Promise { + if (changesInfo.hasChanges) { + await this.cleanProject(platform, appFilesUpdaterOptions, platformData, projectData); } - let directoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); - let excludedDirs = [constants.APP_RESOURCES_FOLDER_NAME]; - if (!changesInfo || !changesInfo.modulesChanged) { - excludedDirs.push(constants.TNS_MODULES_FOLDER_NAME); + if (!changesInfo || changesInfo.changesRequirePrepare) { + await this.copyAppFiles(platformData, appFilesUpdaterOptions, projectData); + this.copyAppResources(platformData, projectData); + await platformData.platformProjectService.prepareProject(projectData, platformSpecificData); } - this.$projectFilesManager.processPlatformSpecificFiles(directoryPath, platform, excludedDirs); + if (!changesInfo || changesInfo.modulesChanged || appFilesUpdaterOptions.bundle) { + let appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); + let lastModifiedTime = this.$fs.exists(appDestinationDirectoryPath) ? this.$fs.getFsStats(appDestinationDirectoryPath).mtime : null; + + let tnsModulesDestinationPath = path.join(appDestinationDirectoryPath, constants.TNS_MODULES_FOLDER_NAME); + // Process node_modules folder + await this.$nodeModulesBuilder.prepareNodeModules(tnsModulesDestinationPath, platform, lastModifiedTime, projectData); + } if (!changesInfo || changesInfo.configChanged || changesInfo.modulesChanged) { await platformData.platformProjectService.processConfigurationFilesFromAppResources(appFilesUpdaterOptions.release, projectData); } platformData.platformProjectService.interpolateConfigurationFile(projectData, platformSpecificData); - - this.$logger.out("Project successfully prepared (" + platform + ")"); + this.$projectChangesService.setNativePlatformStatus(platform, projectData, + { nativePlatformStatus: constants.NativePlatformStatus.alreadyPrepared }); } - private async copyAppFiles(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, projectData: IProjectData): Promise { - let platformData = this.$platformsData.getPlatformData(platform, projectData); + private async copyAppFiles(platformData: IPlatformData, appFilesUpdaterOptions: IAppFilesUpdaterOptions, projectData: IProjectData): Promise { platformData.platformProjectService.ensureConfigurationFileInAppResources(projectData); let appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); @@ -351,8 +372,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { }); } - private copyAppResources(platform: string, projectData: IProjectData): void { - let platformData = this.$platformsData.getPlatformData(platform, projectData); + private copyAppResources(platformData: IPlatformData, projectData: IProjectData): void { let appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); let appResourcesDirectoryPath = path.join(appDestinationDirectoryPath, constants.APP_RESOURCES_FOLDER_NAME); if (this.$fs.exists(appResourcesDirectoryPath)) { @@ -364,15 +384,14 @@ export class PlatformService extends EventEmitter implements IPlatformService { } } - private async copyTnsModules(platform: string, projectData: IProjectData): Promise { - let platformData = this.$platformsData.getPlatformData(platform, projectData); + private async copyTnsModules(platform: string, platformData: IPlatformData, projectData: IProjectData): Promise { let appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); let lastModifiedTime = this.$fs.exists(appDestinationDirectoryPath) ? this.$fs.getFsStats(appDestinationDirectoryPath).mtime : null; try { let tnsModulesDestinationPath = path.join(appDestinationDirectoryPath, constants.TNS_MODULES_FOLDER_NAME); // Process node_modules folder - await this.$nodeModulesBuilder.prepareNodeModules(tnsModulesDestinationPath, platform, lastModifiedTime, projectData); + await this.$nodeModulesBuilder.prepareJSNodeModules(tnsModulesDestinationPath, platform, lastModifiedTime, projectData); } catch (error) { this.$logger.debug(error); shell.rm("-rf", appDestinationDirectoryPath); @@ -715,9 +734,19 @@ export class PlatformService extends EventEmitter implements IPlatformService { } } - public async ensurePlatformInstalled(platform: string, platformTemplate: string, projectData: IProjectData, config: IAddPlatformCoreOptions): Promise { + public async ensurePlatformInstalled(platform: string, platformTemplate: string, projectData: IProjectData, config: IAddPlatformCoreOptions, nativePrepare?: INativePrepare): Promise { + let requiresNativePlatformAdd = false; + if (!this.isPlatformInstalled(platform, projectData)) { - await this.addPlatform(platform, platformTemplate, projectData, config); + await this.addPlatform(platform, platformTemplate, projectData, config, "", nativePrepare); + } else { + const shouldAddNativePlatform = !nativePrepare || !nativePrepare.skipNativePrepare; + const prepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData); + // 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); + } } } diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index 38faf585e5..57d07d132b 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -122,7 +122,7 @@ export class PluginsService implements IPluginsService { } } - private preparePluginScripts(pluginData: IPluginData, platform: string, projectData: IProjectData): void { + public preparePluginScripts(pluginData: IPluginData, platform: string, projectData: IProjectData): void { let platformData = this.$platformsData.getPlatformData(platform, projectData); let pluginScriptsDestinationPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME, "tns_modules"); let scriptsDestinationExists = this.$fs.exists(pluginScriptsDestinationPath); diff --git a/lib/services/project-changes-service.ts b/lib/services/project-changes-service.ts index c28b4272b8..77c6d536ee 100644 --- a/lib/services/project-changes-service.ts +++ b/lib/services/project-changes-service.ts @@ -1,5 +1,6 @@ import * as path from "path"; -import { NODE_MODULES_FOLDER_NAME } from "../constants"; +import { NODE_MODULES_FOLDER_NAME, NativePlatformStatus, PACKAGE_JSON_FILE_NAME } from "../constants"; +import { getHash } from "../common/helpers"; const prepareInfoFileName = ".nsprepareinfo"; @@ -12,6 +13,7 @@ class ProjectChangesInfo implements IProjectChangesInfo { public packageChanged: boolean; public nativeChanged: boolean; public signingChanged: boolean; + public nativePlatformStatus: NativePlatformStatus; public get hasChanges(): boolean { return this.packageChanged || @@ -58,7 +60,7 @@ export class ProjectChangesService implements IProjectChangesService { if (!this.ensurePrepareInfo(platform, projectData, projectChangesOptions)) { this._newFiles = 0; this._changesInfo.appFilesChanged = this.containsNewerFiles(projectData.appDirectoryPath, projectData.appResourcesDirectoryPath, projectData); - this._changesInfo.packageChanged = this.filesChanged([path.join(projectData.projectDir, "package.json")]); + this._changesInfo.packageChanged = this.isProjectFileChanged(projectData, platform); this._changesInfo.appResourcesChanged = this.containsNewerFiles(projectData.appResourcesDirectoryPath, null, projectData); /*done because currently all node_modules are traversed, a possible improvement could be traversing only the production dependencies*/ this._changesInfo.nativeChanged = this.containsNewerFiles( @@ -72,8 +74,8 @@ export class ProjectChangesService implements IProjectChangesService { let platformResourcesDir = path.join(projectData.appResourcesDirectoryPath, platformData.normalizedPlatformName); if (platform === this.$devicePlatformsConstants.iOS.toLowerCase()) { this._changesInfo.configChanged = this.filesChanged([path.join(platformResourcesDir, platformData.configurationFileName), - path.join(platformResourcesDir, "LaunchScreen.storyboard"), - path.join(platformResourcesDir, "build.xcconfig") + path.join(platformResourcesDir, "LaunchScreen.storyboard"), + path.join(platformResourcesDir, "build.xcconfig") ]); } else { this._changesInfo.configChanged = this.filesChanged([ @@ -107,6 +109,9 @@ export class ProjectChangesService implements IProjectChangesService { this._prepareInfo.changesRequireBuildTime = this._prepareInfo.time; } } + + this._changesInfo.nativePlatformStatus = this._prepareInfo.nativePlatformStatus; + return this._changesInfo; } @@ -134,24 +139,41 @@ export class ProjectChangesService implements IProjectChangesService { this.$fs.writeJson(prepareInfoFilePath, this._prepareInfo); } + public setNativePlatformStatus(platform: string, projectData: IProjectData, addedPlatform: IAddedNativePlatform): void { + this._prepareInfo = this._prepareInfo || this.getPrepareInfo(platform, projectData); + if (this._prepareInfo) { + this._prepareInfo.nativePlatformStatus = addedPlatform.nativePlatformStatus; + this.savePrepareInfo(platform, projectData); + } + } + private ensurePrepareInfo(platform: string, projectData: IProjectData, projectChangesOptions: IProjectChangesOptions): boolean { this._prepareInfo = this.getPrepareInfo(platform, projectData); if (this._prepareInfo) { + this._prepareInfo.nativePlatformStatus = this._prepareInfo.nativePlatformStatus && this._prepareInfo.nativePlatformStatus < projectChangesOptions.nativePlatformStatus ? + projectChangesOptions.nativePlatformStatus : + this._prepareInfo.nativePlatformStatus || projectChangesOptions.nativePlatformStatus; + let platformData = this.$platformsData.getPlatformData(platform, projectData); let prepareInfoFile = path.join(platformData.projectRoot, prepareInfoFileName); this._outputProjectMtime = this.$fs.getFsStats(prepareInfoFile).mtime.getTime(); this._outputProjectCTime = this.$fs.getFsStats(prepareInfoFile).ctime.getTime(); return false; } + this._prepareInfo = { time: "", + nativePlatformStatus: projectChangesOptions.nativePlatformStatus, bundle: projectChangesOptions.bundle, release: projectChangesOptions.release, changesRequireBuild: true, + projectFileHash: this.getProjectFileStrippedHash(projectData, platform), 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; @@ -159,6 +181,27 @@ export class ProjectChangesService implements IProjectChangesService { return true; } + private getProjectFileStrippedHash(projectData: IProjectData, platform: string): string { + platform = platform.toLowerCase(); + const projectFilePath = path.join(projectData.projectDir, PACKAGE_JSON_FILE_NAME); + const projectFileContents = this.$fs.readJson(projectFilePath); + _(this.$devicePlatformsConstants) + .keys() + .map(k => k.toLowerCase()) + .difference([platform]) + .each(otherPlatform => { + delete projectFileContents.nativescript[`tns-${otherPlatform}`]; + }); + + return getHash(JSON.stringify(projectFileContents)); + } + + private isProjectFileChanged(projectData: IProjectData, platform: string): boolean { + const projectFileStrippedContentsHash = this.getProjectFileStrippedHash(projectData, platform); + const prepareInfo = this.getPrepareInfo(platform, projectData); + return projectFileStrippedContentsHash !== prepareInfo.projectFileHash; + } + private filesChanged(files: string[]): boolean { for (let file of files) { if (this.$fs.exists(file)) { @@ -168,6 +211,7 @@ export class ProjectChangesService implements IProjectChangesService { } } } + return false; } diff --git a/lib/tools/node-modules/node-modules-builder.ts b/lib/tools/node-modules/node-modules-builder.ts index ac1556151d..0143d437ab 100644 --- a/lib/tools/node-modules/node-modules-builder.ts +++ b/lib/tools/node-modules/node-modules-builder.ts @@ -9,13 +9,29 @@ export class NodeModulesBuilder implements INodeModulesBuilder { ) { } public async prepareNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime: Date, projectData: IProjectData): Promise { + const productionDependencies = this.initialPrepareNodeModules(absoluteOutputPath, platform, lastModifiedTime, projectData); + const npmPluginPrepare: NpmPluginPrepare = this.$injector.resolve(NpmPluginPrepare); + await npmPluginPrepare.preparePlugins(productionDependencies, platform, projectData); + } + + public async prepareJSNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime: Date, projectData: IProjectData): Promise { + const productionDependencies = this.initialPrepareNodeModules(absoluteOutputPath, platform, lastModifiedTime, projectData); + const npmPluginPrepare: NpmPluginPrepare = this.$injector.resolve(NpmPluginPrepare); + await npmPluginPrepare.prepareJSPlugins(productionDependencies, platform, projectData); + } + + public cleanNodeModules(absoluteOutputPath: string, platform: string): void { + shelljs.rm("-rf", absoluteOutputPath); + } + + private initialPrepareNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime: Date, projectData: IProjectData, ): IDependencyData[] { + const productionDependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir); + if (!this.$fs.exists(absoluteOutputPath)) { // Force copying if the destination doesn't exist. lastModifiedTime = null; } - let productionDependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir); - if (!this.$options.bundle) { const tnsModulesCopy = this.$injector.resolve(TnsModulesCopy, { outputRoot: absoluteOutputPath @@ -25,12 +41,7 @@ export class NodeModulesBuilder implements INodeModulesBuilder { this.cleanNodeModules(absoluteOutputPath, platform); } - const npmPluginPrepare: NpmPluginPrepare = this.$injector.resolve(NpmPluginPrepare); - await npmPluginPrepare.preparePlugins(productionDependencies, platform, projectData); - } - - public cleanNodeModules(absoluteOutputPath: string, platform: string): void { - shelljs.rm("-rf", absoluteOutputPath); + return productionDependencies; } } diff --git a/lib/tools/node-modules/node-modules-dest-copy.ts b/lib/tools/node-modules/node-modules-dest-copy.ts index a66fb2eebe..78d19c607c 100644 --- a/lib/tools/node-modules/node-modules-dest-copy.ts +++ b/lib/tools/node-modules/node-modules-dest-copy.ts @@ -81,7 +81,8 @@ export class NpmPluginPrepare { constructor( private $fs: IFileSystem, private $pluginsService: IPluginsService, - private $platformsData: IPlatformsData + private $platformsData: IPlatformsData, + private $logger: ILogger ) { } @@ -146,10 +147,33 @@ export class NpmPluginPrepare { const dependency = dependencies[dependencyKey]; let isPlugin = !!dependency.nativescript; if (isPlugin) { - await this.$pluginsService.prepare(dependency, platform, projectData); + let 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): Promise { + if (_.isEmpty(dependencies) || this.allPrepared(dependencies, platform, projectData)) { + return; + } + + for (let dependencyKey in dependencies) { + const dependency = dependencies[dependencyKey]; + let isPlugin = !!dependency.nativescript; + if (isPlugin) { + platform = platform.toLowerCase(); + let pluginData = this.$pluginsService.convertToPluginData(dependency, projectData.projectDir); + let platformData = this.$platformsData.getPlatformData(platform, projectData); + let appFolderExists = this.$fs.exists(path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME)); + if (appFolderExists) { + this.$pluginsService.preparePluginScripts(pluginData, platform, projectData); + // Show message + this.$logger.out(`Successfully prepared plugin ${pluginData.name} for ${platform}.`); + } + } + } + } } diff --git a/test/debug.ts b/test/debug.ts index d204cb3576..995c73c429 100644 --- a/test/debug.ts +++ b/test/debug.ts @@ -1,6 +1,6 @@ import * as stubs from "./stubs"; import * as yok from "../lib/common/yok"; -import { DebugAndroidCommand } from "../lib/commands/debug"; +import { DebugAndroidCommand, DebugPlatformCommand } from "../lib/commands/debug"; import { assert } from "chai"; import { Configuration, StaticConfig } from "../lib/config"; import { Options } from "../lib/options"; @@ -68,6 +68,11 @@ function createTestInjector(): IInjector { testInjector.register("prompter", {}); testInjector.registerCommand("debug|android", DebugAndroidCommand); + testInjector.register("liveSyncCommandHelper", { + getDevicesLiveSyncInfo: async (): Promise => { + return null; + } + }); return testInjector; } @@ -78,7 +83,7 @@ describe("debug command tests", () => { const testInjector = createTestInjector(); const options = testInjector.resolve("options"); options.forDevice = options.emulator = true; - const debugCommand = testInjector.resolveCommand("debug|android"); + const debugCommand = testInjector.resolve(DebugPlatformCommand, { debugService: {}, platform: "android" }); await assert.isRejected(debugCommand.getDeviceForDebug(), DebugCommandErrors.UNABLE_TO_USE_FOR_DEVICE_AND_EMULATOR); }); @@ -95,7 +100,7 @@ describe("debug command tests", () => { const options = testInjector.resolve("options"); options.device = specifiedDeviceOption; - const debugCommand = testInjector.resolveCommand("debug|android"); + const debugCommand = testInjector.resolve(DebugPlatformCommand, { debugService: {}, platform: "android" }); const selectedDeviceInstance = await debugCommand.getDeviceForDebug(); assert.deepEqual(selectedDeviceInstance, deviceInstance); }); @@ -111,7 +116,7 @@ describe("debug command tests", () => { const devicesService = testInjector.resolve("devicesService"); devicesService.getDeviceInstances = (): Mobile.IDevice[] => getDeviceInstancesResult; - const debugCommand = testInjector.resolveCommand("debug|android"); + const debugCommand = testInjector.resolve(DebugPlatformCommand, { debugService: {}, platform: "android" }); await assert.isRejected(debugCommand.getDeviceForDebug(), DebugCommandErrors.NO_DEVICES_EMULATORS_FOUND_FOR_OPTIONS); }; @@ -184,7 +189,7 @@ describe("debug command tests", () => { const devicesService = testInjector.resolve("devicesService"); devicesService.getDeviceInstances = (): Mobile.IDevice[] => [deviceInstance]; - const debugCommand = testInjector.resolveCommand("debug|android"); + const debugCommand = testInjector.resolve(DebugPlatformCommand, { debugService: {}, platform: "android" }); const actualDeviceInstance = await debugCommand.getDeviceForDebug(); assert.deepEqual(actualDeviceInstance, deviceInstance); }); @@ -243,7 +248,7 @@ describe("debug command tests", () => { return choices[1]; }; - const debugCommand = testInjector.resolveCommand("debug|android"); + const debugCommand = testInjector.resolve(DebugPlatformCommand, { debugService: {}, platform: "android" }); const actualDeviceInstance = await debugCommand.getDeviceForDebug(); const expectedChoicesPassedToPrompter = [deviceInstance1, deviceInstance2].map(d => `${d.deviceInfo.identifier} - ${d.deviceInfo.displayName}`); assert.deepEqual(choicesPassedToPrompter, expectedChoicesPassedToPrompter); @@ -304,7 +309,7 @@ describe("debug command tests", () => { devicesService.getDeviceInstances = (): Mobile.IDevice[] => deviceInstances; - const debugCommand = testInjector.resolveCommand("debug|android"); + const debugCommand = testInjector.resolve(DebugPlatformCommand, { debugService: {}, platform: "android" }); const actualDeviceInstance = await debugCommand.getDeviceForDebug(); assert.deepEqual(actualDeviceInstance, deviceInstance2); diff --git a/test/platform-service.ts b/test/platform-service.ts index 94ae456ac5..d017e07c76 100644 --- a/test/platform-service.ts +++ b/test/platform-service.ts @@ -49,6 +49,9 @@ function createTestInjector() { testInjector.register("nodeModulesBuilder", { prepareNodeModules: () => { return Promise.resolve(); + }, + prepareJSNodeModules: () => { + return Promise.resolve(); } }); testInjector.register("pluginsService", { @@ -382,6 +385,12 @@ describe('Platform Service Tests', () => { let appDestFolderPath = path.join(tempFolder, "appDest"); let appResourcesFolderPath = path.join(appDestFolderPath, "App_Resources"); + fs.writeJson(path.join(tempFolder, "package.json"), { + name: "testname", + nativescript: { + id: "org.nativescript.testname" + } + }); return { tempFolder, appFolderPath, app1FolderPath, appDestFolderPath, appResourcesFolderPath }; } diff --git a/test/plugin-prepare.ts b/test/plugin-prepare.ts index 45f3664147..6c07491663 100644 --- a/test/plugin-prepare.ts +++ b/test/plugin-prepare.ts @@ -7,7 +7,7 @@ class TestNpmPluginPrepare extends NpmPluginPrepare { public preparedDependencies: IDictionary = {}; constructor(private previouslyPrepared: IDictionary) { - super(null, null, null); + super(null, null, null, null); } protected getPreviouslyPreparedDependencies(platform: string): IDictionary { diff --git a/test/project-changes-service.ts b/test/project-changes-service.ts index 5fab8b9598..3593400340 100644 --- a/test/project-changes-service.ts +++ b/test/project-changes-service.ts @@ -32,6 +32,13 @@ class ProjectChangesServiceTest extends BaseServiceTest { this.injector.register("devicePlatformsConstants", {}); this.injector.register("projectChangesService", ProjectChangesService); + const fs = this.injector.resolve("fs"); + fs.writeJson(path.join(this.projectDir, Constants.PACKAGE_JSON_FILE_NAME), { + nativescript: { + id: "org.nativescript.test" + } + }); + } get projectChangesService(): IProjectChangesService { @@ -113,13 +120,15 @@ describe("Project Changes Service Tests", () => { // arrange const prepareInfoPath = path.join(serviceTest.projectDir, Constants.PLATFORMS_DIR_NAME, platform, ".nsprepareinfo"); - const expectedPrepareInfo = { + const expectedPrepareInfo: IPrepareInfo = { time: new Date().toString(), bundle: true, release: false, changesRequireBuild: true, changesRequireBuildTime: new Date().toString(), - iOSProvisioningProfileUUID: "provisioning_profile_test" + iOSProvisioningProfileUUID: "provisioning_profile_test", + projectFileHash: "", + nativePlatformStatus: Constants.NativePlatformStatus.requiresPlatformAdd }; fs.writeJson(prepareInfoPath, expectedPrepareInfo); diff --git a/test/stubs.ts b/test/stubs.ts index 8fa4e100a7..1a4902a444 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -206,7 +206,7 @@ export class ErrorsStub implements IErrors { } export class NpmInstallationManagerStub implements INpmInstallationManager { - async install(packageName: string, pathToSave?: string, options?: INpmInstallOptions ): Promise { + async install(packageName: string, pathToSave?: string, options?: INpmInstallOptions): Promise { return Promise.resolve(""); } @@ -574,6 +574,10 @@ export class ProjectChangesService implements IProjectChangesService { public get currentChanges(): IProjectChangesInfo { return {}; } + + public setNativePlatformStatus(platform: string, projectData: IProjectData, nativePlatformStatus: IAddedNativePlatform): void { + return; + } } export class CommandsService implements ICommandsService {