diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 770a938183..b6283ba8f9 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -134,7 +134,8 @@ $injector.require("usbLiveSyncService", "./services/livesync/livesync-service"); $injector.require("previewAppLiveSyncService", "./services/livesync/playground/preview-app-livesync-service"); $injector.require("previewAppPluginsService", "./services/livesync/playground/preview-app-plugins-service"); $injector.require("previewSdkService", "./services/livesync/playground/preview-sdk-service"); -$injector.require("playgroundQrCodeGenerator", "./services/livesync/playground/qr-code-generator"); +$injector.requirePublicClass("previewDevicesService", "./services/livesync/playground/devices/preview-devices-service"); +$injector.requirePublic("previewQrCodeService", "./services/livesync/playground/preview-qr-code-service"); $injector.requirePublic("sysInfo", "./sys-info"); $injector.require("iOSNotificationService", "./services/ios-notification-service"); diff --git a/lib/commands/preview.ts b/lib/commands/preview.ts index 45d6639ff8..bc42413b3f 100644 --- a/lib/commands/preview.ts +++ b/lib/commands/preview.ts @@ -3,11 +3,12 @@ export class PreviewCommand implements ICommand { private static MIN_SUPPORTED_WEBPACK_VERSION = "0.17.0"; constructor(private $bundleValidatorHelper: IBundleValidatorHelper, + private $errors: IErrors, private $liveSyncService: ILiveSyncService, private $networkConnectivityValidator: INetworkConnectivityValidator, private $projectData: IProjectData, private $options: IOptions, - private $playgroundQrCodeGenerator: IPlaygroundQrCodeGenerator) { } + private $previewQrCodeService: IPreviewQrCodeService) { } public async execute(): Promise { await this.$liveSyncService.liveSync([], { @@ -23,10 +24,14 @@ export class PreviewCommand implements ICommand { useHotModuleReload: this.$options.hmr }); - await this.$playgroundQrCodeGenerator.generateQrCode({ useHotModuleReload: this.$options.hmr, link: this.$options.link }); + await this.$previewQrCodeService.printLiveSyncQrCode({ useHotModuleReload: this.$options.hmr, link: this.$options.link }); } public async canExecute(args: string[]): Promise { + if (args && args.length) { + this.$errors.fail(`The arguments '${args.join(" ")}' are not valid for the preview command.`); + } + await this.$networkConnectivityValidator.validate(); this.$bundleValidatorHelper.validate(PreviewCommand.MIN_SUPPORTED_WEBPACK_VERSION); return true; diff --git a/lib/commands/test-init.ts b/lib/commands/test-init.ts index 10099c9461..2f8921ed9c 100644 --- a/lib/commands/test-init.ts +++ b/lib/commands/test-init.ts @@ -93,9 +93,10 @@ class TestInitCommand implements ICommand { await this.$pluginsService.add('nativescript-unit-test-runner', this.$projectData); const testsDir = path.join(this.$projectData.appDirectoryPath, 'tests'); + const relativeTestsDir = path.relative(this.$projectData.projectDir, testsDir); let shouldCreateSampleTests = true; if (this.$fs.exists(testsDir)) { - this.$logger.info('app/tests/ directory already exists, will not create an example test project.'); + this.$logger.info(`${relativeTestsDir} directory already exists, will not create an example test project.`); shouldCreateSampleTests = false; } @@ -104,8 +105,9 @@ class TestInitCommand implements ICommand { const frameworks = [frameworkToInstall].concat(this.karmaConfigAdditionalFrameworks[frameworkToInstall] || []) .map(fw => `'${fw}'`) .join(', '); + const testFiles = `'${relativeTestsDir}/**/*.js'`; const karmaConfTemplate = this.$resources.readText('test/karma.conf.js'); - const karmaConf = _.template(karmaConfTemplate)({ frameworks }); + const karmaConf = _.template(karmaConfTemplate)({ frameworks, testFiles }); this.$fs.writeFile(path.join(projectDir, 'karma.conf.js'), karmaConf); @@ -113,9 +115,9 @@ class TestInitCommand implements ICommand { if (shouldCreateSampleTests && this.$fs.exists(exampleFilePath)) { this.$fs.copyFile(exampleFilePath, path.join(testsDir, 'example.js')); - this.$logger.info('\nExample test file created in app/tests/'.yellow); + this.$logger.info(`\nExample test file created in ${relativeTestsDir}`.yellow); } else { - this.$logger.info('\nPlace your test files under app/tests/'.yellow); + this.$logger.info(`\nPlace your test files under ${relativeTestsDir}`.yellow); } this.$logger.info('Run your tests using the "$ tns test " command.'.yellow); diff --git a/lib/common/constants.ts b/lib/common/constants.ts index b3b1b5bcab..272291a758 100644 --- a/lib/common/constants.ts +++ b/lib/common/constants.ts @@ -155,5 +155,5 @@ export class AndroidVirtualDevice { static TIMEOUT_SECONDS = 120; static GENYMOTION_DEFAULT_STDERR_STRING = "Logging activities to file"; - static UNABLE_TO_START_EMULATOR_MESSAGE = "Cannot run your app in the native emulator. Increase the timeout of the operation with the --timeout option or try to restart your adb server with 'adb kill-server' command. Alternatively, run the Android Virtual Device manager and increase the allocated RAM for the virtual device."; + static UNABLE_TO_START_EMULATOR_MESSAGE = "Cannot run the app in the selected native emulator. Try to restart the adb server by running the `adb kill-server` command in the Command Prompt, or increase the allocated RAM of the virtual device through the Android Virtual Device manager. NativeScript CLI users can try to increase the timeout of the operation by adding the `--timeout` flag."; } diff --git a/lib/common/declarations.d.ts b/lib/common/declarations.d.ts index 8aedd1092c..06bd4e3061 100644 --- a/lib/common/declarations.d.ts +++ b/lib/common/declarations.d.ts @@ -969,6 +969,21 @@ interface IQrCodeGenerator { generateDataUri(data: string): Promise; } +interface IQrCodeImageData { + /** + * The original URL used for generating QR code image. + */ + originalUrl: string; + /** + * The shorten URL used for generating QR code image. + */ + shortenUrl: string; + /** + * Base64 encoded data used for generating QR code image. + */ + imageData: string; +} + interface IDynamicHelpProvider { /** * Checks if current project's framework is one of the specified as arguments. diff --git a/lib/common/definitions/mobile.d.ts b/lib/common/definitions/mobile.d.ts index 3fa23012fa..6a25892a7c 100644 --- a/lib/common/definitions/mobile.d.ts +++ b/lib/common/definitions/mobile.d.ts @@ -143,10 +143,6 @@ declare module Mobile { --predicate 'eventType == logEvent and subsystem contains "com.example.my_subsystem"' */ predicate?: string; - /** - * If set to true, device's log will not be displayed on the console. - */ - muted?: boolean; } interface IDeviceAppData extends IPlatform, IConnectTimeoutOption { @@ -221,12 +217,18 @@ declare module Mobile { * @param {string} projectName The project name of the currently running application for which we need the logs. */ setProjectNameForDevice(deviceIdentifier: string, projectName: string): void; + + /** + * Disables logs on the specified device and does not print any logs on the console. + * @param {string} deviceIdentifier The unique identifier of the device. + */ + muteLogsForDevice(deviceIdentifier: string): void; } /** * Describes different options for filtering device logs. */ - interface IDeviceLogOptions extends IStringDictionary { + interface IDeviceLogOptions extends IDictionary { /** * Process id of the application on the device. */ @@ -241,6 +243,11 @@ declare module Mobile { * The project name. */ projectName?: string; + + /** + * Specifies if the logs will be printed on the console. + */ + muteLogs?: boolean; } /** @@ -730,6 +737,12 @@ declare module Mobile { * @returns {Promise} Starts the emulator and returns the errors if some error occurs. */ startEmulator(options: Mobile.IStartEmulatorOptions): Promise; + + /** + * Called when emulator is lost. Its purpose is to clean any resources used by the instance. + * @returns {void} + */ + detach?(deviceInfo: Mobile.IDeviceInfo): void; } interface IStartEmulatorOutput { @@ -772,6 +785,11 @@ declare module Mobile { * @param imageIdentifier - The imagerIdentifier of the emulator. */ startEmulatorArgs(imageIdentifier: string): string[]; + /** + * Called when emulator is lost. Its purpose is to clean any resources used by the instance. + * @returns {void} + */ + detach?(deviceInfo: Mobile.IDeviceInfo): void; } interface IVirtualBoxService { diff --git a/lib/common/mobile/android/android-device.ts b/lib/common/mobile/android/android-device.ts index 1a2b87cc1d..33fa17a2d5 100644 --- a/lib/common/mobile/android/android-device.ts +++ b/lib/common/mobile/android/android-device.ts @@ -121,6 +121,12 @@ export class AndroidDevice implements Mobile.IAndroidDevice { } } + public detach(): void { + if (this.isEmulator) { + this.$androidEmulatorServices.detach(this.deviceInfo); + } + } + private async getDeviceDetails(shellCommandArgs: string[]): Promise { const parsedDetails: any = {}; diff --git a/lib/common/mobile/android/android-emulator-services.ts b/lib/common/mobile/android/android-emulator-services.ts index 48648c50be..0b193cd135 100644 --- a/lib/common/mobile/android/android-emulator-services.ts +++ b/lib/common/mobile/android/android-emulator-services.ts @@ -59,6 +59,10 @@ export class AndroidEmulatorServices implements Mobile.IEmulatorPlatformService }; } + public detach(deviceInfo: Mobile.IDeviceInfo) { + this.$androidVirtualDeviceService.detach(deviceInfo); + } + private async startEmulatorCore(options: Mobile.IAndroidStartEmulatorOptions): Promise<{runningEmulator: Mobile.IDeviceInfo, errors: string[], endTimeEpoch: number}> { const timeout = options.timeout || AndroidVirtualDevice.TIMEOUT_SECONDS; const endTimeEpoch = getCurrentEpochTime() + this.$utils.getMilliSecondsTimeout(timeout); diff --git a/lib/common/mobile/android/android-virtual-device-service.ts b/lib/common/mobile/android/android-virtual-device-service.ts index a81a391c6c..3aeb0fbe8f 100644 --- a/lib/common/mobile/android/android-virtual-device-service.ts +++ b/lib/common/mobile/android/android-virtual-device-service.ts @@ -142,9 +142,16 @@ export class AndroidVirtualDeviceService implements Mobile.IAndroidVirtualDevice }); } + public detach(deviceInfo: Mobile.IDeviceInfo) { + if (this.mapEmulatorIdToImageIdentifier[deviceInfo.identifier]) { + delete this.mapEmulatorIdToImageIdentifier[deviceInfo.identifier]; + } + } + private async getEmulatorImagesCore(): Promise { let result: ISpawnResult = null; let devices: Mobile.IDeviceInfo[] = []; + let errors: string[] = []; if (this.pathToAvdManagerExecutable && this.$fs.exists(this.pathToAvdManagerExecutable)) { result = await this.$childProcess.trySpawnFromCloseEvent(this.pathToAvdManagerExecutable, ["list", "avds"]); @@ -154,11 +161,12 @@ export class AndroidVirtualDeviceService implements Mobile.IAndroidVirtualDevice if (result && result.stdout) { devices = this.parseListAvdsOutput(result.stdout); + errors = result && result.stderr ? [result.stderr] : []; } else { devices = this.listAvdsFromDirectory(); } - return { devices, errors: result && result.stderr ? [result.stderr] : [] }; + return { devices, errors }; } private async getRunningEmulatorData(runningEmulatorId: string, availableEmulators: Mobile.IDeviceInfo[]): Promise { diff --git a/lib/common/mobile/android/logcat-helper.ts b/lib/common/mobile/android/logcat-helper.ts index 127c2c7799..d662733afe 100644 --- a/lib/common/mobile/android/logcat-helper.ts +++ b/lib/common/mobile/android/logcat-helper.ts @@ -58,20 +58,6 @@ export class LogcatHelper implements Mobile.ILogcatHelper { } } - private async getLogcatStream(deviceIdentifier: string, pid?: string) { - const device = await this.$devicesService.getDevice(deviceIdentifier); - const minAndroidWithLogcatPidSupport = "7.0.0"; - const isLogcatPidSupported = semver.gte(semver.coerce(device.deviceInfo.version), minAndroidWithLogcatPidSupport); - const adb: Mobile.IDeviceAndroidDebugBridge = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: deviceIdentifier }); - const logcatCommand = ["logcat"]; - - if (pid && isLogcatPidSupported) { - logcatCommand.push(`--pid=${pid}`); - } - const logcatStream = await adb.executeCommand(logcatCommand, { returnChildProcess: true }); - return logcatStream; - } - public async dump(deviceIdentifier: string): Promise { const adb: Mobile.IDeviceAndroidDebugBridge = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: deviceIdentifier }); const logcatDumpStream = await adb.executeCommand(["logcat", "-d"], { returnChildProcess: true }); @@ -101,6 +87,20 @@ export class LogcatHelper implements Mobile.ILogcatHelper { delete this.mapDevicesLoggingData[deviceIdentifier]; } } + + private async getLogcatStream(deviceIdentifier: string, pid?: string) { + const device = await this.$devicesService.getDevice(deviceIdentifier); + const minAndroidWithLogcatPidSupport = "7.0.0"; + const isLogcatPidSupported = !!device.deviceInfo.version && semver.gte(semver.coerce(device.deviceInfo.version), minAndroidWithLogcatPidSupport); + const adb: Mobile.IDeviceAndroidDebugBridge = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: deviceIdentifier }); + const logcatCommand = ["logcat"]; + + if (pid && isLogcatPidSupported) { + logcatCommand.push(`--pid=${pid}`); + } + const logcatStream = await adb.executeCommand(logcatCommand, { returnChildProcess: true }); + return logcatStream; + } } $injector.register("logcatHelper", LogcatHelper); diff --git a/lib/common/mobile/device-log-provider-base.ts b/lib/common/mobile/device-log-provider-base.ts index 4662b4fbe0..9954d599bb 100644 --- a/lib/common/mobile/device-log-provider-base.ts +++ b/lib/common/mobile/device-log-provider-base.ts @@ -26,6 +26,10 @@ export abstract class DeviceLogProviderBase extends EventEmitter implements Mobi this.setLogLevel(logLevel, deviceIdentifier); } + public muteLogsForDevice(deviceIdentifier: string): void { + this.setDeviceLogOptionsProperty(deviceIdentifier, (deviceLogOptions: Mobile.IDeviceLogOptions) => deviceLogOptions.muteLogs, true); + } + protected getApplicationPidForDevice(deviceIdentifier: string): string { return this.devicesLogOptions[deviceIdentifier] && this.devicesLogOptions[deviceIdentifier].applicationPid; } @@ -39,7 +43,7 @@ export abstract class DeviceLogProviderBase extends EventEmitter implements Mobi return this.devicesLogOptions[deviceIdentifier]; } - protected setDeviceLogOptionsProperty(deviceIdentifier: string, propNameFunction: Function, propertyValue: string): void { + protected setDeviceLogOptionsProperty(deviceIdentifier: string, propNameFunction: Function, propertyValue: string | boolean): void { const propertyName = getPropertyName(propNameFunction); if (propertyName) { diff --git a/lib/common/mobile/device-log-provider.ts b/lib/common/mobile/device-log-provider.ts index d6dded6d46..6a83624eb2 100644 --- a/lib/common/mobile/device-log-provider.ts +++ b/lib/common/mobile/device-log-provider.ts @@ -11,7 +11,7 @@ export class DeviceLogProvider extends DeviceLogProviderBase { const loggingOptions = this.getDeviceLogOptionsForDevice(deviceIdentifier); const data = this.$logFilter.filterData(platform, lineText, loggingOptions); if (data) { - this.$logger.write(data); + this.logDataCore(data, loggingOptions); this.emit(DEVICE_LOG_EVENT_NAME, lineText, deviceIdentifier, platform); } } @@ -19,5 +19,11 @@ export class DeviceLogProvider extends DeviceLogProviderBase { public setLogLevel(logLevel: string, deviceIdentifier?: string): void { this.$logFilter.loggingLevel = logLevel.toUpperCase(); } + + private logDataCore(data: string, loggingOptions: Mobile.IDeviceLogOptions): void { + if (!loggingOptions || (loggingOptions && !loggingOptions.muteLogs)) { + this.$logger.write(data); + } + } } $injector.register("deviceLogProvider", DeviceLogProvider); diff --git a/lib/common/test/unit-tests/mobile/android-virtual-device-service.ts b/lib/common/test/unit-tests/mobile/android-virtual-device-service.ts index 3130cb51a4..9cf9795fe3 100644 --- a/lib/common/test/unit-tests/mobile/android-virtual-device-service.ts +++ b/lib/common/test/unit-tests/mobile/android-virtual-device-service.ts @@ -176,14 +176,13 @@ describe("androidVirtualDeviceService", () => { assert.deepEqual(result.devices, []); assert.deepEqual(result.errors, []); }); - it("should return an empty array when `avdmanager list avds` command fails", async () => { + it("should return an empty array and no errors when `avdmanager list avds` command fails", async () => { const avdManagerError = "some error while executing avdmanager list avds"; const avdService = mockAvdService({ avdManagerError }); const result = await avdService.getEmulatorImages([]); assert.lengthOf(result.devices, 0); assert.deepEqual(result.devices, []); - assert.lengthOf(result.errors, 1); - assert.deepEqual(result.errors, [avdManagerError]); + assert.lengthOf(result.errors, 0); }); it("should return all emulators when there are available emulators and no running emulators", async () => { const avdService = mockAvdService({ @@ -214,6 +213,24 @@ describe("androidVirtualDeviceService", () => { assert.deepEqual(result[1], getAvailableEmulatorData({ displayName: "Nexus_5X_API_28", imageIdentifier: "Nexus_5X_API_28", version: "9.0.0", model: "Nexus 5X" })); assert.deepEqual(result[2], getAvailableEmulatorData({ displayName: "Nexus_6P_API_28", imageIdentifier: "Nexus_6P_API_28", version: "9.0.0", model: "Nexus 6P" })); }); + // In this case we should fallback to list avd directory and should't report errors from avdmanager + it("should return devices and no errors when there is an error on avdmanager's stderr", async () => { + const iniFilesData = getIniFilesData(); + const testInjector = createTestInjector({ + avdManagerOutput: "", + avdManagerError: "my test error", + iniFilesData + }); + + const fs = testInjector.resolve("fs"); + fs.readDirectory = () => _.keys(iniFilesData); + + const avdService = testInjector.resolve("androidVirtualDeviceService"); + const result = await avdService.getEmulatorImages(["emulator-5554 device"]); + + assert.deepEqual(result.devices.length, 3); + assert.deepEqual(result.errors.length, 0); + }); }); describe("when avdmanager is not found", () => { diff --git a/lib/common/test/unit-tests/stubs.ts b/lib/common/test/unit-tests/stubs.ts index f1f9709623..52f17bc965 100644 --- a/lib/common/test/unit-tests/stubs.ts +++ b/lib/common/test/unit-tests/stubs.ts @@ -171,4 +171,6 @@ export class DeviceLogProviderStub extends EventEmitter implements Mobile.IDevic setProjectNameForDevice(deviceIdentifier: string, projectName: string): void { this.currentDeviceProjectNames[deviceIdentifier] = projectName; } + + muteLogsForDevice(deviceIdentifier: string): void { } } diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index ef54b193b8..67ecb3381f 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -709,7 +709,7 @@ interface IiOSNotification extends NodeJS.EventEmitter { } interface IiOSSocketRequestExecutor { - executeLaunchRequest(deviceIdentifier: string, timeout: number, readyForAttachTimeout: number, projectId: string, shouldBreak?: boolean): Promise; + executeLaunchRequest(deviceIdentifier: string, timeout: number, readyForAttachTimeout: number, projectId: string, debugOptions: IDebugOptions): Promise; executeAttachRequest(device: Mobile.IiOSDevice, timeout: number, projectId: string): Promise; } diff --git a/lib/definitions/debug.d.ts b/lib/definitions/debug.d.ts index 034f3403b6..350094fa27 100644 --- a/lib/definitions/debug.d.ts +++ b/lib/definitions/debug.d.ts @@ -95,6 +95,10 @@ interface IDebugOptions { * The sdk version of the emulator. */ sdk?: string; + /** + * Defines if the handshake(AppLaunching notification) between CLI and runtime should be executed. The handshake is not needed when CLI retries to attach to the debugger. + */ + skipHandshake?: boolean; } /** diff --git a/lib/definitions/preview-app-livesync.d.ts b/lib/definitions/preview-app-livesync.d.ts index 81907e41ef..bbee82aa56 100644 --- a/lib/definitions/preview-app-livesync.d.ts +++ b/lib/definitions/preview-app-livesync.d.ts @@ -12,7 +12,6 @@ declare global { interface IPreviewSdkService extends EventEmitter { getQrCodeUrl(options: IHasUseHotModuleReloadOption): string; - connectedDevices: Device[]; initialize(getInitialFiles: (device: Device) => Promise): void; applyChanges(filesPayload: FilesPayload): Promise; stop(): void; @@ -23,15 +22,27 @@ declare global { getExternalPlugins(device: Device): string[]; } - interface IPlaygroundQrCodeGenerator { - generateQrCode(options: IGenerateQrCodeOptions): Promise; + interface IPreviewQrCodeService { + getPlaygroundAppQrCode(options?: IPlaygroundAppQrCodeOptions): Promise>; + printLiveSyncQrCode(options: IPrintLiveSyncOptions): Promise; } - interface IGenerateQrCodeOptions extends IHasUseHotModuleReloadOption { + interface IPlaygroundAppQrCodeOptions { + platform?: string; + } + + interface IPrintLiveSyncOptions extends IHasUseHotModuleReloadOption { /** * If set to true, a link will be shown on console instead of QR code * Default value is false. */ link: boolean; } + + interface IPreviewDevicesService extends EventEmitter { + getConnectedDevices(): Device[]; + updateConnectedDevices(devices: Device[]): void; + getDeviceById(id: string): Device; + getDevicesForPlatform(platform: string): Device[]; + } } \ No newline at end of file diff --git a/lib/device-sockets/ios/socket-request-executor.ts b/lib/device-sockets/ios/socket-request-executor.ts index 88bb8e5bc4..76db790060 100644 --- a/lib/device-sockets/ios/socket-request-executor.ts +++ b/lib/device-sockets/ios/socket-request-executor.ts @@ -48,24 +48,19 @@ export class IOSSocketRequestExecutor implements IiOSSocketRequestExecutor { } } - public async executeLaunchRequest(deviceIdentifier: string, timeout: number, readyForAttachTimeout: number, projectId: string, shouldBreak?: boolean): Promise { + public async executeLaunchRequest(deviceIdentifier: string, timeout: number, readyForAttachTimeout: number, projectId: string, debugOptions: IDebugOptions): Promise { try { - const appLaunchingSocket = await this.$iOSNotificationService.postNotification(deviceIdentifier, this.$iOSNotification.getAppLaunching(projectId), constants.IOS_OBSERVE_NOTIFICATION_COMMAND_TYPE); - await this.$iOSNotificationService.awaitNotification(deviceIdentifier, +appLaunchingSocket, timeout); + if (!debugOptions.skipHandshake) { + await this.executeHandshake(deviceIdentifier, projectId, timeout); + } - if (shouldBreak) { + if (debugOptions.debugBrk) { await this.$iOSNotificationService.postNotification(deviceIdentifier, this.$iOSNotification.getWaitForDebug(projectId)); } - // We need to send the ObserveNotification ReadyForAttach before we post the AttachRequest. - const readyForAttachSocket = await this.$iOSNotificationService.postNotification(deviceIdentifier, this.$iOSNotification.getReadyForAttach(projectId), constants.IOS_OBSERVE_NOTIFICATION_COMMAND_TYPE); - const readyForAttachPromise = this.$iOSNotificationService.awaitNotification(deviceIdentifier, +readyForAttachSocket, readyForAttachTimeout); - - await this.$iOSNotificationService.postNotification(deviceIdentifier, this.$iOSNotification.getAttachRequest(projectId, deviceIdentifier)); - await readyForAttachPromise; + await this.executeAttachAvailable(deviceIdentifier, projectId, readyForAttachTimeout); } catch (e) { - this.$logger.trace("Launch request error:"); - this.$logger.trace(e); + this.$logger.trace("Launch request error: ", e); this.$errors.failWithoutHelp("Error while waiting for response from NativeScript runtime."); } } @@ -79,9 +74,18 @@ export class IOSSocketRequestExecutor implements IiOSSocketRequestExecutor { await this.$iOSNotificationService.postNotification(deviceIdentifier, this.$iOSNotification.getAttachRequest(projectId, deviceIdentifier)); await readyForAttachPromise; } catch (e) { + this.$logger.trace("Attach available error: ", e); this.$errors.failWithoutHelp(`The application ${projectId} timed out when performing the socket handshake.`); } } + + private async executeHandshake(deviceIdentifier: string, projectId: string, timeout: number): Promise { + // This notification will be send only once by the runtime during application start. + // In case app is already running, we'll fail here as we'll not receive it. + const appLaunchingNotification = this.$iOSNotification.getAppLaunching(projectId); + const appLaunchingSocket = await this.$iOSNotificationService.postNotification(deviceIdentifier, appLaunchingNotification, constants.IOS_OBSERVE_NOTIFICATION_COMMAND_TYPE); + await this.$iOSNotificationService.awaitNotification(deviceIdentifier, +appLaunchingSocket, timeout); + } } $injector.register("iOSSocketRequestExecutor", IOSSocketRequestExecutor); diff --git a/lib/services/ios-debug-service.ts b/lib/services/ios-debug-service.ts index 94648ecc54..91234ee881 100644 --- a/lib/services/ios-debug-service.ts +++ b/lib/services/ios-debug-service.ts @@ -50,20 +50,7 @@ export class IOSDebugService extends DebugServiceBase implements IPlatformDebugS debugOptions.emulator = true; } - if (!debugOptions.justlaunch) { - let projectName = debugData.projectName; - if (!projectName && debugData.projectDir) { - const projectData = this.$projectDataService.getProjectData(debugData.projectDir); - projectName = projectData.projectName; - } - - if (projectName) { - this.$deviceLogProvider.setProjectNameForDevice(debugData.deviceIdentifier, projectName); - } - - await this.device.openDeviceLogStream({ predicate: IOS_LOG_PREDICATE }); - } - + await this.startDeviceLogProcess(debugData, debugOptions); await this.$iOSDebuggerPortService.attachToDebuggerPortFoundEvent(this.device, debugData, debugOptions); if (debugOptions.emulator) { @@ -113,6 +100,26 @@ export class IOSDebugService extends DebugServiceBase implements IPlatformDebugS return chromeDebugUrl; } + private async startDeviceLogProcess(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + if (debugOptions.justlaunch) { + // No logs should be printed on console when `--justlaunch` option is passed. + // On the other side we need to start log process in order to get debugger port from logs. + this.$deviceLogProvider.muteLogsForDevice(debugData.deviceIdentifier); + } + + let projectName = debugData.projectName; + if (!projectName && debugData.projectDir) { + const projectData = this.$projectDataService.getProjectData(debugData.projectDir); + projectName = projectData.projectName; + } + + if (projectName) { + this.$deviceLogProvider.setProjectNameForDevice(debugData.deviceIdentifier, projectName); + } + + await this.device.openDeviceLogStream({ predicate: IOS_LOG_PREDICATE }); + } + private async killProcess(childProcess: ChildProcess): Promise { if (childProcess) { return new Promise((resolve, reject) => { @@ -182,7 +189,7 @@ export class IOSDebugService extends DebugServiceBase implements IPlatformDebugS } private async debugBrkCore(device: Mobile.IiOSDevice, debugData: IDebugData, debugOptions: IDebugOptions): Promise { - await this.$iOSSocketRequestExecutor.executeLaunchRequest(device.deviceInfo.identifier, AWAIT_NOTIFICATION_TIMEOUT_SECONDS, AWAIT_NOTIFICATION_TIMEOUT_SECONDS, debugData.applicationIdentifier, debugOptions.debugBrk); + await this.$iOSSocketRequestExecutor.executeLaunchRequest(device.deviceInfo.identifier, AWAIT_NOTIFICATION_TIMEOUT_SECONDS, AWAIT_NOTIFICATION_TIMEOUT_SECONDS, debugData.applicationIdentifier, debugOptions); return this.wireDebuggerClient(debugData, debugOptions, device); } diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 655b3dabea..5054ef5ae1 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -1290,7 +1290,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f if (!teamId) { const teams = await this.$iOSProvisionService.getDevelopmentTeams(); - this.$logger.warn("Xcode 8 requires a team id to be specified when building for device."); + 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; diff --git a/lib/services/livesync/livesync-service.ts b/lib/services/livesync/livesync-service.ts index 56566b8d9e..3a6cf0b68a 100644 --- a/lib/services/livesync/livesync-service.ts +++ b/lib/services/livesync/livesync-service.ts @@ -238,7 +238,9 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi }; debugData.pathToAppPackage = this.$platformService.lastOutputPath(settings.platform, buildConfig, projectData, settings.outputPath); - return this.printDebugInformation(await this.$debugService.debug(debugData, settings.debugOptions)); + const debugInfo = await this.$debugService.debug(debugData, settings.debugOptions); + const result = this.printDebugInformation(debugInfo); + return result; } public printDebugInformation(debugInformation: IDebugInformation): IDebugInformation { @@ -284,6 +286,7 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi } catch (err) { this.$logger.trace("Couldn't attach debugger, will modify options and try again.", err); attachDebuggerOptions.debugOptions.start = false; + attachDebuggerOptions.debugOptions.skipHandshake = true; try { debugInformation = await this.attachDebugger(attachDebuggerOptions); } catch (innerErr) { @@ -700,7 +703,8 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi rebuiltInformation, projectData, deviceBuildInfoDescriptor, - liveSyncData, + // the clean option should be respected only during initial sync + liveSyncData: _.assign({}, liveSyncData, { clean: false }), settings: latestAppPackageInstalledSettings, modifiedFiles: allModifiedFiles, filesToRemove: currentFilesToRemove, diff --git a/lib/services/livesync/playground/devices/preview-devices-service.ts b/lib/services/livesync/playground/devices/preview-devices-service.ts new file mode 100644 index 0000000000..09e18f275a --- /dev/null +++ b/lib/services/livesync/playground/devices/preview-devices-service.ts @@ -0,0 +1,40 @@ +import { Device } from "nativescript-preview-sdk"; +import { EventEmitter } from "events"; +import { DeviceDiscoveryEventNames } from "../../../../common/constants"; + +export class PreviewDevicesService extends EventEmitter implements IPreviewDevicesService { + private connectedDevices: Device[] = []; + + public getConnectedDevices(): Device[] { + return this.connectedDevices; + } + + public updateConnectedDevices(devices: Device[]): void { + _(devices) + .reject(d => _.find(this.connectedDevices, device => d.id === device.id)) + .each(device => this.raiseDeviceFound(device)); + + _(this.connectedDevices) + .reject(d => _.find(devices, device => d.id === device.id)) + .each(device => this.raiseDeviceLost(device)); + } + + public getDeviceById(id: string): Device { + return _.find(this.connectedDevices, { id }); + } + + public getDevicesForPlatform(platform: string): Device[] { + return _.filter(this.connectedDevices, { platform: platform.toLowerCase() }); + } + + private raiseDeviceFound(device: Device) { + this.emit(DeviceDiscoveryEventNames.DEVICE_FOUND, device); + this.connectedDevices.push(device); + } + + private raiseDeviceLost(device: Device) { + this.emit(DeviceDiscoveryEventNames.DEVICE_LOST, device); + _.remove(this.connectedDevices, d => d.id === device.id); + } +} +$injector.register("previewDevicesService", PreviewDevicesService); diff --git a/lib/services/livesync/playground/preview-app-livesync-service.ts b/lib/services/livesync/playground/preview-app-livesync-service.ts index 8ea4af7c54..a1727430a0 100644 --- a/lib/services/livesync/playground/preview-app-livesync-service.ts +++ b/lib/services/livesync/playground/preview-app-livesync-service.ts @@ -28,6 +28,7 @@ export class PreviewAppLiveSyncService implements IPreviewAppLiveSyncService { private $projectDataService: IProjectDataService, private $previewSdkService: IPreviewSdkService, private $previewAppPluginsService: IPreviewAppPluginsService, + private $previewDevicesService: IPreviewDevicesService, private $projectFilesManager: IProjectFilesManager, private $hmrStatusService: IHmrStatusService, private $projectFilesProvider: IProjectFilesProvider) { } @@ -52,64 +53,18 @@ export class PreviewAppLiveSyncService implements IPreviewAppLiveSyncService { }); } - private async initializePreviewForDevice(data: IPreviewAppLiveSyncData, device: Device): Promise { - const filesToSyncMap: IDictionary = {}; - const hmrData: IDictionary = {}; - let promise = Promise.resolve(null); - const startSyncFilesTimeout = async (platform: string) => { - await promise - .then(async () => { - const currentHmrData = _.cloneDeep(hmrData); - const platformHmrData = currentHmrData[platform] || {}; - const filesToSync = _.cloneDeep(filesToSyncMap[platform]); - // We don't need to prepare when webpack emits changed files. We just need to send a message to pubnub. - promise = this.syncFilesForPlatformSafe(data, platform, { filesToSync, skipPrepare: true, useHotModuleReload: data.appFilesUpdaterOptions.useHotModuleReload }); - await promise; - - if (data.appFilesUpdaterOptions.useHotModuleReload && platformHmrData.hash) { - const devices = _.filter(this.$previewSdkService.connectedDevices, { platform: platform.toLowerCase() }); - - await Promise.all(_.map(devices, async (previewDevice: Device) => { - const status = await this.$hmrStatusService.getHmrStatus(previewDevice.id, platformHmrData.hash); - if (status === HmrConstants.HMR_ERROR_STATUS) { - await this.syncFilesForPlatformSafe(data, platform, { filesToSync: platformHmrData.fallbackFiles, useHotModuleReload: false, deviceId: previewDevice.id }); - } - })); - } - }); - filesToSyncMap[platform] = []; - }; - await this.$hooksService.executeBeforeHooks("preview-sync", { - hookArgs: { - projectData: this.$projectDataService.getProjectData(data.projectDir), - hmrData, - config: { - env: data.env, - platform: device.platform, - appFilesUpdaterOptions: data.appFilesUpdaterOptions, - }, - externals: this.$previewAppPluginsService.getExternalPlugins(device), - filesToSyncMap, - startSyncFilesTimeout: startSyncFilesTimeout.bind(this) - } - }); - await this.$previewAppPluginsService.comparePluginsOnDevice(data, device); - const payloads = await this.syncFilesForPlatformSafe(data, device.platform, { isInitialSync: true, useHotModuleReload: data.appFilesUpdaterOptions.useHotModuleReload }); - return payloads; - } - public async syncFiles(data: IPreviewAppLiveSyncData, filesToSync: string[], filesToRemove: string[]): Promise { this.showWarningsForNativeFiles(filesToSync); - for (const device of this.$previewSdkService.connectedDevices) { + const connectedDevices = this.$previewDevicesService.getConnectedDevices(); + for (const device of connectedDevices) { await this.$previewAppPluginsService.comparePluginsOnDevice(data, device); } - const platforms = _(this.$previewSdkService.connectedDevices) + const platforms = _(connectedDevices) .map(device => device.platform) .uniq() .value(); - for (const platform of platforms) { await this.syncFilesForPlatformSafe(data, platform, { filesToSync, filesToRemove, useHotModuleReload: data.appFilesUpdaterOptions.useHotModuleReload }); } @@ -119,6 +74,58 @@ export class PreviewAppLiveSyncService implements IPreviewAppLiveSyncService { this.$previewSdkService.stop(); } + private async initializePreviewForDevice(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.syncFilesForPlatformSafe(data, device.platform, { isInitialSync: true, useHotModuleReload: data.appFilesUpdaterOptions.useHotModuleReload }); + return payloads; + } + + private getHookArgs(data: IPreviewAppLiveSyncData, device: Device) { + const filesToSyncMap: IDictionary = {}; + const hmrData: IDictionary = {}; + const promise = Promise.resolve(null); + const result = { + projectData: this.$projectDataService.getProjectData(data.projectDir), + hmrData, + config: { + env: data.env, + platform: device.platform, + appFilesUpdaterOptions: data.appFilesUpdaterOptions, + }, + externals: this.$previewAppPluginsService.getExternalPlugins(device), + filesToSyncMap, + startSyncFilesTimeout: async (platform: string) => await this.onWebpackCompilationComplete(data, hmrData, filesToSyncMap, promise, platform) + }; + + return result; + } + + 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 filesToSync = _.cloneDeep(filesToSyncMap[platform]); + // We don't need to prepare when webpack emits changed files. We just need to send a message to pubnub. + promise = this.syncFilesForPlatformSafe(data, platform, { filesToSync, skipPrepare: true, useHotModuleReload: data.appFilesUpdaterOptions.useHotModuleReload }); + await promise; + + if (data.appFilesUpdaterOptions.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) { + await this.syncFilesForPlatformSafe(data, platform, { filesToSync: platformHmrData.fallbackFiles, useHotModuleReload: false, deviceId: previewDevice.id }); + } + })); + } + }); + filesToSyncMap[platform] = []; + } + private async syncFilesForPlatformSafe(data: IPreviewAppLiveSyncData, platform: string, opts?: ISyncFilesOptions): Promise { this.$logger.info(`Start syncing changes for platform ${platform}.`); diff --git a/lib/services/livesync/playground/preview-app-plugins-service.ts b/lib/services/livesync/playground/preview-app-plugins-service.ts index 41a2480cfd..ecf7fc0b57 100644 --- a/lib/services/livesync/playground/preview-app-plugins-service.ts +++ b/lib/services/livesync/playground/preview-app-plugins-service.ts @@ -74,20 +74,25 @@ export class PreviewAppPluginsService implements IPreviewAppPluginsService { private getWarningForPluginCore(localPlugin: string, localPluginVersion: string, devicePluginVersion: string, deviceId: string): string { this.$logger.trace(`Comparing plugin ${localPlugin} with localPluginVersion ${localPluginVersion} and devicePluginVersion ${devicePluginVersion}`); - if (devicePluginVersion) { - const localPluginVersionData = semver.coerce(localPluginVersion); - const devicePluginVersionData = semver.coerce(devicePluginVersion); - - if (localPluginVersionData.major !== devicePluginVersionData.major) { - return util.format(PluginComparisonMessages.LOCAL_PLUGIN_WITH_DIFFERENCE_IN_MAJOR_VERSION, localPlugin, localPluginVersion, devicePluginVersion); - } else if (localPluginVersionData.minor > devicePluginVersionData.minor) { - return util.format(PluginComparisonMessages.LOCAL_PLUGIN_WITH_GREATHER_MINOR_VERSION, localPlugin, localPluginVersion, devicePluginVersion); - } + if (!devicePluginVersion) { + return util.format(PluginComparisonMessages.PLUGIN_NOT_INCLUDED_IN_PREVIEW_APP, localPlugin, deviceId); + } + const shouldSkipCheck = !semver.valid(localPluginVersion) && !semver.validRange(localPluginVersion); + if (shouldSkipCheck) { return null; } - return util.format(PluginComparisonMessages.PLUGIN_NOT_INCLUDED_IN_PREVIEW_APP, localPlugin, deviceId); + const localPluginVersionData = semver.coerce(localPluginVersion); + const devicePluginVersionData = semver.coerce(devicePluginVersion); + + if (localPluginVersionData.major !== devicePluginVersionData.major) { + return util.format(PluginComparisonMessages.LOCAL_PLUGIN_WITH_DIFFERENCE_IN_MAJOR_VERSION, localPlugin, localPluginVersion, devicePluginVersion); + } else if (localPluginVersionData.minor > devicePluginVersionData.minor) { + return util.format(PluginComparisonMessages.LOCAL_PLUGIN_WITH_GREATHER_MINOR_VERSION, localPlugin, localPluginVersion, devicePluginVersion); + } + + return null; } private hasNativeCode(localPlugin: string, platform: string, projectDir: string): boolean { diff --git a/lib/services/livesync/playground/preview-qr-code-service.ts b/lib/services/livesync/playground/preview-qr-code-service.ts new file mode 100644 index 0000000000..3421fe4903 --- /dev/null +++ b/lib/services/livesync/playground/preview-qr-code-service.ts @@ -0,0 +1,77 @@ +import * as util from "util"; +import { EOL } from "os"; +import { PlaygroundStoreUrls } from "./preview-app-constants"; +import { exported } from "../../../common/decorators"; + +export class PreviewQrCodeService implements IPreviewQrCodeService { + constructor( + private $config: IConfiguration, + private $httpClient: Server.IHttpClient, + private $logger: ILogger, + private $mobileHelper: Mobile.IMobileHelper, + private $previewSdkService: IPreviewSdkService, + private $qrCodeTerminalService: IQrCodeTerminalService, + private $qr: IQrCodeGenerator + ) { } + + @exported("previewQrCodeService") + public async getPlaygroundAppQrCode(options?: IPlaygroundAppQrCodeOptions): Promise> { + const result = Object.create(null); + + if (!options || !options.platform || this.$mobileHelper.isAndroidPlatform(options.platform)) { + result.android = await this.getQrCodeImageData(PlaygroundStoreUrls.GOOGLE_PLAY_URL); + } + + if (!options || !options.platform || this.$mobileHelper.isiOSPlatform(options.platform)) { + result.ios = await this.getQrCodeImageData(PlaygroundStoreUrls.APP_STORE_URL); + } + + return result; + } + + public async printLiveSyncQrCode(options: IPrintLiveSyncOptions): Promise { + const qrCodeUrl = this.$previewSdkService.getQrCodeUrl(options); + const url = await this.getShortenUrl(qrCodeUrl); + + this.$logger.info(); + const message = `${EOL} Generating qrcode for url ${url}.`; + this.$logger.trace(message); + + if (options.link) { + this.$logger.printMarkdown(message); + } else { + this.$qrCodeTerminalService.generate(url); + + this.$logger.info(); + this.$logger.printMarkdown(`# Use \`NativeScript Playground app\` and scan the \`QR code\` above to preview the application on your device.`); + this.$logger.printMarkdown(` +To scan the QR code and deploy your app on a device, you need to have the \`NativeScript Playground app\`: + App Store (iOS): ${PlaygroundStoreUrls.APP_STORE_URL} + Google Play (Android): ${PlaygroundStoreUrls.GOOGLE_PLAY_URL}`); + } + } + + private async getShortenUrl(url: string): Promise { + const shortenUrlEndpoint = util.format(this.$config.SHORTEN_URL_ENDPOINT, encodeURIComponent(url)); + try { + const response = await this.$httpClient.httpRequest(shortenUrlEndpoint); + const responseBody = JSON.parse(response.body); + url = responseBody.shortURL || url; + } catch (e) { + // use the longUrl + } + + return url; + } + + private async getQrCodeImageData(url: string): Promise { + const shortenUrl = await this.getShortenUrl(url); + const imageData = await this.$qr.generateDataUri(shortenUrl); + return { + originalUrl: url, + shortenUrl, + imageData + }; + } +} +$injector.register("previewQrCodeService", PreviewQrCodeService); diff --git a/lib/services/livesync/playground/preview-sdk-service.ts b/lib/services/livesync/playground/preview-sdk-service.ts index df72e02298..603f1bbae5 100644 --- a/lib/services/livesync/playground/preview-sdk-service.ts +++ b/lib/services/livesync/playground/preview-sdk-service.ts @@ -1,18 +1,18 @@ import { MessagingService, Config, Device, DeviceConnectedMessage, SdkCallbacks, ConnectedDevices, FilesPayload } from "nativescript-preview-sdk"; import { PubnubKeys } from "./preview-app-constants"; -import { DEVICE_LOG_EVENT_NAME } from "../../../common/constants"; import { EventEmitter } from "events"; +import { DEVICE_LOG_EVENT_NAME } from "../../../common/constants"; const pako = require("pako"); export class PreviewSdkService extends EventEmitter implements IPreviewSdkService { private static MAX_FILES_UPLOAD_BYTE_LENGTH = 15 * 1024 * 1024; // In MBs private messagingService: MessagingService = null; private instanceId: string = null; - public connectedDevices: Device[] = []; - constructor(private $logger: ILogger, + constructor(private $config: IConfiguration, private $httpClient: Server.IHttpClient, - private $config: IConfiguration) { + private $logger: ILogger, + private $previewDevicesService: IPreviewDevicesService) { super(); } @@ -60,9 +60,8 @@ export class PreviewSdkService extends EventEmitter implements IPreviewSdkServic onLogSdkMessage: (log: string) => { this.$logger.trace("Received onLogSdkMessage message: ", log); }, - onConnectedDevicesChange: (connectedDevices: ConnectedDevices) => ({ }), onLogMessage: (log: string, deviceName: string, deviceId: string) => { - const device = _.find(this.connectedDevices, { id: deviceId}); + const device = this.$previewDevicesService.getDeviceById(deviceId); this.emit(DEVICE_LOG_EVENT_NAME, log, deviceId, device ? device.platform : ""); this.$logger.info(`LOG from device ${deviceName}: ${log}`); }, @@ -72,13 +71,10 @@ export class PreviewSdkService extends EventEmitter implements IPreviewSdkServic onUncaughtErrorMessage: () => { this.$logger.warn("The Preview app has terminated unexpectedly. Please run it again to get a detailed crash report."); }, - onDeviceConnectedMessage: (deviceConnectedMessage: DeviceConnectedMessage) => ({ }), - onDeviceConnected: (device: Device) => { - if (!_.find(this.connectedDevices, {id: device.id})) { - this.connectedDevices.push(device); - } - }, - onDevicesPresence: (devices: Device[]) => ({ }), + onConnectedDevicesChange: (connectedDevices: ConnectedDevices) => ({}), + onDeviceConnectedMessage: (deviceConnectedMessage: DeviceConnectedMessage) => ({}), + onDeviceConnected: (device: Device) => ({}), + onDevicesPresence: (devices: Device[]) => this.$previewDevicesService.updateConnectedDevices(devices), onSendingChange: (sending: boolean) => ({ }), onBiggerFilesUpload: async (filesContent, callback) => { const gzippedContent = Buffer.from(pako.gzip(filesContent)); diff --git a/lib/services/livesync/playground/qr-code-generator.ts b/lib/services/livesync/playground/qr-code-generator.ts deleted file mode 100644 index cea5810478..0000000000 --- a/lib/services/livesync/playground/qr-code-generator.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as util from "util"; -import { EOL } from "os"; -import { PlaygroundStoreUrls } from "./preview-app-constants"; - -export class PlaygroundQrCodeGenerator implements IPlaygroundQrCodeGenerator { - constructor(private $previewSdkService: IPreviewSdkService, - private $httpClient: Server.IHttpClient, - private $qrCodeTerminalService: IQrCodeTerminalService, - private $config: IConfiguration, - private $logger: ILogger) { - } - - public async generateQrCode(options: IGenerateQrCodeOptions): Promise { - let url = this.$previewSdkService.getQrCodeUrl(options); - const shortenUrlEndpoint = util.format(this.$config.SHORTEN_URL_ENDPOINT, encodeURIComponent(url)); - try { - const response = await this.$httpClient.httpRequest(shortenUrlEndpoint); - const responseBody = JSON.parse(response.body); - url = responseBody.shortURL || url; - } catch (e) { - // use the longUrl - } - - this.$logger.info(); - const message = `${EOL} Generating qrcode for url ${url}.`; - this.$logger.trace(message); - - if (options.link) { - this.$logger.printMarkdown(message); - } else { - this.$qrCodeTerminalService.generate(url); - - this.$logger.info(); - this.$logger.printMarkdown(`# Use \`NativeScript Playground app\` and scan the \`QR code\` above to preview the application on your device.`); - this.$logger.printMarkdown(` -To scan the QR code and deploy your app on a device, you need to have the \`NativeScript Playground app\`: - App Store (iOS): ${PlaygroundStoreUrls.APP_STORE_URL} - Google Play (Android): ${PlaygroundStoreUrls.GOOGLE_PLAY_URL}`); - } - } -} -$injector.register("playgroundQrCodeGenerator", PlaygroundQrCodeGenerator); diff --git a/lib/services/platform-environment-requirements.ts b/lib/services/platform-environment-requirements.ts index 284a399006..4cbd86c4ad 100644 --- a/lib/services/platform-environment-requirements.ts +++ b/lib/services/platform-environment-requirements.ts @@ -13,7 +13,7 @@ export class PlatformEnvironmentRequirements implements IPlatformEnvironmentRequ private $staticConfig: IStaticConfig, private $analyticsService: IAnalyticsService, private $injector: IInjector, - private $playgroundQrCodeGenerator: IPlaygroundQrCodeGenerator) { } + private $previewQrCodeService: IPreviewQrCodeService) { } @cache() private get $liveSyncService(): ILiveSyncService { @@ -194,7 +194,7 @@ export class PlatformEnvironmentRequirements implements IPlatformEnvironmentRequ useHotModuleReload: options.hmr }); - await this.$playgroundQrCodeGenerator.generateQrCode({ useHotModuleReload: options.hmr, link: options.link }); + await this.$previewQrCodeService.printLiveSyncQrCode({ useHotModuleReload: options.hmr, link: options.link }); } } diff --git a/lib/services/project-templates-service.ts b/lib/services/project-templates-service.ts index fba5c5cdd8..712202148f 100644 --- a/lib/services/project-templates-service.ts +++ b/lib/services/project-templates-service.ts @@ -30,6 +30,10 @@ export class ProjectTemplatesService implements IProjectTemplatesService { } const templateName = constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()] || name; + if (!this.$fs.exists(templateName)) { + version = version || await this.$npmInstallationManager.getLatestCompatibleVersion(templateName); + } + const fullTemplateName = version ? `${templateName}@${version}` : templateName; const templatePackageJsonContent = await this.getTemplatePackageJsonContent(fullTemplateName); const templateVersion = await this.getTemplateVersion(fullTemplateName); diff --git a/resources/test/karma.conf.js b/resources/test/karma.conf.js index 0dd269a0ac..5fc70a7fb1 100644 --- a/resources/test/karma.conf.js +++ b/resources/test/karma.conf.js @@ -12,7 +12,7 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ - 'app/**/*.js', + ${ testFiles } ], diff --git a/test/ios-project-service.ts b/test/ios-project-service.ts index 1d538e3439..ac4c60a342 100644 --- a/test/ios-project-service.ts +++ b/test/ios-project-service.ts @@ -71,7 +71,7 @@ function createTestInjector(projectPath: string, projectName: string, xcode?: IX projectPath: projectPath, projectFilePath: path.join(projectPath, "package.json"), projectId: "", - projectIdentifiers: { android: "", ios: ""} + projectIdentifiers: { android: "", ios: "" } }); testInjector.register("projectData", projectData); testInjector.register("projectHelper", {}); @@ -505,7 +505,7 @@ describe("Source code in plugin support", () => { const mockPrepareMethods = ["prepareFrameworks", "prepareStaticLibs", "prepareResources", "prepareNativeSourceCode"]; mockPrepareMethods.filter(m => m !== prepareMethodToCall).forEach(methodName => { - iOSProjectService[methodName] = (pluginPlatformsFolderPath: string, pluginData: IPluginData): Promise => { + iOSProjectService[methodName] = (pluginPlatformsFolderPath: string, pluginData: IPluginData): Promise => { return Promise.resolve(); }; }); @@ -513,7 +513,7 @@ describe("Source code in plugin support", () => { iOSProjectService.getXcodeprojPath = () => { return path.join(__dirname, "files"); }; - let pbxProj : any; + let pbxProj: any; iOSProjectService.savePbxProj = (project: any): Promise => { pbxProj = project; return Promise.resolve(); @@ -544,7 +544,7 @@ describe("Source code in plugin support", () => { it("adds plugin with Source files", async () => { const sourceFileNames = [ - "src/Header.h", "src/ObjC.m", + "src/Header.h", "src/ObjC.m", "src/nested/Header.hpp", "src/nested/Source.cpp", "src/nested/ObjCpp.mm", "src/nested/level2/Header2.hxx", "src/nested/level2/Source2.cxx", "src/nested/level2/Source3.c", "src/SomeOtherExtension.donotadd", @@ -949,7 +949,7 @@ describe("Merge Project XCConfig files", () => { appResourcesXcconfigPath = path.join(projectData.appResourcesDirectoryPath, "iOS", BUILD_XCCONFIG_FILE_NAME); appResourceXCConfigContent = `CODE_SIGN_IDENTITY = iPhone Distribution - // To build for device with XCode 8 you need to specify your development team. More info: https://developer.apple.com/library/prerelease/content/releasenotes/DeveloperTools/RN-Xcode/Introduction.html + // To build for device with XCode you need to specify your development team. More info: https://developer.apple.com/library/prerelease/content/releasenotes/DeveloperTools/RN-Xcode/Introduction.html // DEVELOPMENT_TEAM = YOUR_TEAM_ID; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; diff --git a/test/project-templates-service.ts b/test/project-templates-service.ts index ad9e12b27d..48743641c4 100644 --- a/test/project-templates-service.ts +++ b/test/project-templates-service.ts @@ -8,13 +8,14 @@ import { format } from "util"; let isDeleteDirectoryCalledForNodeModulesDir = false; const nativeScriptValidatedTemplatePath = "nsValidatedTemplatePath"; +const compatibleTemplateVersion = "1.2.3"; function createTestInjector(configuration: { shouldNpmInstallThrow?: boolean, packageJsonContent?: any } = {}): IInjector { const injector = new Yok(); injector.register("errors", stubs.ErrorsStub); injector.register("logger", stubs.LoggerStub); injector.register("fs", { - exists: (pathToCheck: string) => true, + exists: (pathToCheck: string) => false, readJson: (pathToFile: string) => configuration.packageJsonContent || {}, @@ -42,6 +43,9 @@ function createTestInjector(configuration: { shouldNpmInstallThrow?: boolean, pa } return Promise.resolve(nativeScriptValidatedTemplatePath); + }, + getLatestCompatibleVersion: (packageName: string) => { + return compatibleTemplateVersion; } }); @@ -164,7 +168,7 @@ describe("project-templates-service", () => { const fs = testInjector.resolve("fs"); fs.exists = (localPath: string): boolean => path.basename(localPath) !== constants.PACKAGE_JSON_FILE_NAME; const pacoteService = testInjector.resolve("pacoteService"); - pacoteService.manifest = () => Promise.resolve({ }); + pacoteService.manifest = () => Promise.resolve({}); await projectTemplatesService.prepareTemplate(localTemplatePath, "tempFolder"); assert.deepEqual(dataSentToGoogleAnalytics, [ { @@ -215,7 +219,7 @@ describe("project-templates-service", () => { const notSupportedVersionString = "not supported version"; const testInjector = createTestInjector({ packageJsonContent: { nativescript: { templateVersion: notSupportedVersionString } } }); const projectTemplatesService = testInjector.resolve("projectTemplatesService"); - const expectedError = format(constants.ProjectTemplateErrors.InvalidTemplateVersionStringFormat, 'tns-template-hello-world-ts', notSupportedVersionString); + const expectedError = format(constants.ProjectTemplateErrors.InvalidTemplateVersionStringFormat, `tns-template-hello-world-ts@${compatibleTemplateVersion}`, notSupportedVersionString); await assert.isRejected(projectTemplatesService.prepareTemplate("typescript", "tempFolder"), expectedError); }); @@ -238,7 +242,7 @@ describe("project-templates-service", () => { { name: "is correct when scoped package name without version is passed", templateName: "@nativescript/vue-template", - expectedVersion: "", + expectedVersion: compatibleTemplateVersion, expectedTemplateName: "@nativescript/vue-template" }, { diff --git a/test/services/platform-environment-requirements.ts b/test/services/platform-environment-requirements.ts index dce5ab3806..5dab5825ff 100644 --- a/test/services/platform-environment-requirements.ts +++ b/test/services/platform-environment-requirements.ts @@ -28,7 +28,7 @@ function createTestInjector() { testInjector.register("platformEnvironmentRequirements", PlatformEnvironmentRequirements); testInjector.register("staticConfig", { SYS_REQUIREMENTS_LINK: "" }); testInjector.register("nativeScriptCloudExtensionService", {}); - testInjector.register("playgroundQrCodeGenerator", {}); + testInjector.register("previewQrCodeService", {}); return testInjector; } diff --git a/test/services/playground/preview-app-livesync-service.ts b/test/services/playground/preview-app-livesync-service.ts index f93061b347..d4ab2e2551 100644 --- a/test/services/playground/preview-app-livesync-service.ts +++ b/test/services/playground/preview-app-livesync-service.ts @@ -75,7 +75,6 @@ class PreviewSdkServiceMock extends EventEmitter implements IPreviewSdkService { return "my_cool_qr_code_url"; } - public connectedDevices: Device[] = [deviceMockData]; public initialize(getInitialFiles: (device: Device) => Promise) { this.getInitialFiles = async (device) => { const filesPayload = await getInitialFiles(device); @@ -158,6 +157,9 @@ function createTestInjector(options?: { isHookCalledWithHMR = args.hookArgs.config.appFilesUpdaterOptions.useHotModuleReload; } }); + injector.register("previewDevicesService", { + getConnectedDevices: () => [deviceMockData] + }); return injector; } diff --git a/test/services/playground/preview-app-plugins-service.ts b/test/services/playground/preview-app-plugins-service.ts index 1b21282909..13087a0a7f 100644 --- a/test/services/playground/preview-app-plugins-service.ts +++ b/test/services/playground/preview-app-plugins-service.ts @@ -243,6 +243,16 @@ describe("previewAppPluginsService", () => { "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: [] } ]; diff --git a/test/services/preview-devices-service.ts b/test/services/preview-devices-service.ts new file mode 100644 index 0000000000..7fc2305115 --- /dev/null +++ b/test/services/preview-devices-service.ts @@ -0,0 +1,120 @@ +import { Yok } from "../../lib/common/yok"; +import { PreviewDevicesService } from "../../lib/services/livesync/playground/devices/preview-devices-service"; +import { Device } from "nativescript-preview-sdk"; +import { assert } from "chai"; +import { DeviceDiscoveryEventNames } from "../../lib/common/constants"; +import { LoggerStub } from "../stubs"; + +let foundDevices: Device[] = []; +let lostDevices: Device[] = []; + +function createTestInjector(): IInjector { + const injector = new Yok(); + injector.register("previewDevicesService", PreviewDevicesService); + injector.register("logger", LoggerStub); + return injector; +} + +function createDevice(id: string): Device { + return { + id, + platform: "ios", + model: "my test model", + name: "my test name", + osVersion: "10.0.0", + previewAppVersion: "19.0.0", + runtimeVersion: "5.0.0" + }; +} + +function resetDevices() { + foundDevices = []; + lostDevices = []; +} + +describe("PreviewDevicesService", () => { + describe("onDevicesPresence", () => { + let previewDevicesService: IPreviewDevicesService = null; + beforeEach(() => { + const injector = createTestInjector(); + previewDevicesService = injector.resolve("previewDevicesService"); + previewDevicesService.on(DeviceDiscoveryEventNames.DEVICE_FOUND, device => { + foundDevices.push(device); + }); + previewDevicesService.on(DeviceDiscoveryEventNames.DEVICE_LOST, device => { + lostDevices.push(device); + }); + }); + + afterEach(() => { + previewDevicesService.removeAllListeners(); + resetDevices(); + }); + + it("should add new device", () => { + const device = createDevice("device1"); + + previewDevicesService.updateConnectedDevices([device]); + + assert.deepEqual(previewDevicesService.getConnectedDevices(), [device]); + assert.deepEqual(foundDevices, [device]); + assert.deepEqual(lostDevices, []); + }); + it("should add new device when there are already connected devices", () => { + const device1 = createDevice("device1"); + const device2 = createDevice("device2"); + + previewDevicesService.updateConnectedDevices([device1]); + assert.deepEqual(previewDevicesService.getConnectedDevices(), [device1]); + assert.deepEqual(foundDevices, [device1]); + assert.deepEqual(lostDevices, []); + resetDevices(); + + previewDevicesService.updateConnectedDevices([device1, device2]); + + assert.deepEqual(previewDevicesService.getConnectedDevices(), [device1, device2]); + assert.deepEqual(foundDevices, [device2]); + assert.deepEqual(lostDevices, []); + }); + it("should add more than one new device", () => { + const device1 = createDevice("device1"); + const device2 = createDevice("device2"); + const device3 = createDevice("device3"); + + previewDevicesService.updateConnectedDevices([device1, device2, device3]); + + assert.deepEqual(previewDevicesService.getConnectedDevices(), [device1, device2, device3]); + assert.deepEqual(foundDevices, [device1, device2, device3]); + assert.deepEqual(lostDevices, []); + }); + it("should remove device", () => { + const device1 = createDevice("device1"); + previewDevicesService.updateConnectedDevices([device1]); + assert.deepEqual(previewDevicesService.getConnectedDevices(), [device1]); + assert.deepEqual(foundDevices, [device1]); + assert.deepEqual(lostDevices, []); + resetDevices(); + + previewDevicesService.updateConnectedDevices([]); + + assert.deepEqual(foundDevices, []); + assert.deepEqual(lostDevices, [device1]); + }); + it("should add and remove devices in the same time", () => { + const device1 = createDevice("device1"); + const device2 = createDevice("device2"); + + previewDevicesService.updateConnectedDevices([device1]); + assert.deepEqual(previewDevicesService.getConnectedDevices(), [device1]); + assert.deepEqual(foundDevices, [device1]); + assert.deepEqual(lostDevices, []); + resetDevices(); + + previewDevicesService.updateConnectedDevices([device2]); + + assert.deepEqual(previewDevicesService.getConnectedDevices(), [device2]); + assert.deepEqual(foundDevices, [device2]); + assert.deepEqual(lostDevices, [device1]); + }); + }); +}); diff --git a/test/services/preview-sdk-service.ts b/test/services/preview-sdk-service.ts index 0790c0de42..e4698c03e4 100644 --- a/test/services/preview-sdk-service.ts +++ b/test/services/preview-sdk-service.ts @@ -8,7 +8,7 @@ const getPreviewSdkService = (): IPreviewSdkService => { testInjector.register("logger", LoggerStub); testInjector.register("config", {}); testInjector.register("previewSdkService", PreviewSdkService); - + testInjector.register("previewDevicesService", {}); testInjector.register("httpClient", { httpRequest: async (options: any, proxySettings?: IProxySettings): Promise => undefined }); diff --git a/test/xcconfig-service.ts b/test/xcconfig-service.ts index 9a96daecf8..c60c5998e4 100644 --- a/test/xcconfig-service.ts +++ b/test/xcconfig-service.ts @@ -58,7 +58,7 @@ describe("XCConfig Service Tests", () => { return `// You can add custom settings here // for example you can uncomment the following line to force distribution code signing CODE_SIGN_IDENTITY = iPhone Distribution - // To build for device with XCode 8 you need to specify your development team. More info: https://developer.apple.com/library/prerelease/content/releasenotes/DeveloperTools/RN-Xcode/Introduction.html + // To build for device with XCode you need to specify your development team. More info: https://developer.apple.com/library/prerelease/content/releasenotes/DeveloperTools/RN-Xcode/Introduction.html // DEVELOPMENT_TEAM = YOUR_TEAM_ID; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;`; @@ -80,7 +80,7 @@ describe("XCConfig Service Tests", () => { return `// You can add custom settings here // for example you can uncomment the following line to force distribution code signing CODE_SIGN_IDENTITY = iPhone Distribution - // To build for device with XCode 8 you need to specify your development team. More info: https://developer.apple.com/library/prerelease/content/releasenotes/DeveloperTools/RN-Xcode/Introduction.html + // To build for device with XCode you need to specify your development team. More info: https://developer.apple.com/library/prerelease/content/releasenotes/DeveloperTools/RN-Xcode/Introduction.html // DEVELOPMENT_TEAM = YOUR_TEAM_ID ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage`;