import { componentUtil } from '@salesforce/lightning-lsp-common' ; import { EmptyParametersGatherer , SfdxCommandlet , SfdxWorkspaceChecker } from '@salesforce/salesforcedx-utils-vscode/out/src/' ; import { CliCommandExecutor , CommandOutput , SfdxCommandBuilder } from '@salesforce/salesforcedx-utils-vscode/out/src/cli' ; import { notificationService } from '@salesforce/salesforcedx-utils-vscode/out/src/commands' ; import * as fs from 'fs' ; import * as path from 'path' ; import * as vscode from 'vscode' ; import { channelService } from '../channel' ; import { nls } from '../messages' ; import { DevServerService } from '../service/devServerService' ; import { PreviewService } from '../service/previewService' ; import { telemetryService } from '../telemetry' ; import { openBrowser , showError } from './commandUtils' ; import { ForceLightningLwcStartExecutor } from './forceLightningLwcStart' ; enum PreviewPlatformType { Desktop = 1 , Android , iOS } export const enum PlatformName { Desktop = 'Desktop' , Android = 'Android' , iOS = 'iOS' } interface IOSSimulatorDevice { name : string ; udid : string ; state : string ; runtimeId : string ; isAvailable : boolean ; } interface AndroidVirtualDevice { name : string ; displayName : string ; deviceName : string ; path : string ; target : string ; api : string ; } interface PreviewQuickPickItem extends vscode . QuickPickItem { label : string ; detail : string ; alwaysShow : boolean ; picked : boolean ; id : PreviewPlatformType ; defaultTargetName : string ; platformName : keyof typeof PlatformName ; } export interface DeviceQuickPickItem extends vscode . QuickPickItem { name : string ; } export const platformOptions = [ { label : nls . localize ( 'force_lightning_lwc_preview_desktop_label' ) , detail : nls . localize ( 'force_lightning_lwc_preview_desktop_description' ) , alwaysShow : true , picked : true , id : PreviewPlatformType . Desktop , platformName : PlatformName . Desktop , defaultTargetName : '' } , { label : nls . localize ( 'force_lightning_lwc_android_label' ) , detail : nls . localize ( 'force_lightning_lwc_android_description' ) , alwaysShow : true , picked : false , id : PreviewPlatformType . Android , platformName : PlatformName . Android , defaultTargetName : 'SFDXEmulator' } , { label : nls . localize ( 'force_lightning_lwc_ios_label' ) , detail : nls . localize ( 'force_lightning_lwc_ios_description' ) , alwaysShow : true , picked : false , id : PreviewPlatformType . iOS , platformName : PlatformName . iOS , defaultTargetName : 'SFDXSimulator' } ] ; const logName = 'force_lightning_lwc_preview' ; const commandName = nls . localize ( 'force_lightning_lwc_preview_text' ) ; const sfdxDeviceListCommand = 'force:lightning:local:device:list' ; const sfdxMobilePreviewCommand = 'force:lightning:lwc:preview' ; const androidSuccessString = 'Launching... Opening Browser' ; export async function forceLightningLwcPreview ( sourceUri ) { const startTime = process . hrtime ( ) ; if ( ! sourceUri ) { if ( vscode . window . activeTextEditor ) { sourceUri = vscode . window . activeTextEditor . document . uri ; } else { const message = nls . localize ( 'force_lightning_lwc_preview_file_undefined' , sourceUri ) ; showError ( new Error ( message ) , logName , commandName ) ; return ; } } const resourcePath = sourceUri . fsPath ; if ( ! resourcePath ) { const message = nls . localize ( 'force_lightning_lwc_preview_file_undefined' , resourcePath ) ; showError ( new Error ( message ) , logName , commandName ) ; return ; } if ( ! fs . existsSync ( resourcePath ) ) { const message = nls . localize ( 'force_lightning_lwc_preview_file_nonexist' , resourcePath ) ; showError ( new Error ( message ) , logName , commandName ) ; return ; } const isSFDX = true ; const isDirectory = fs . lstatSync ( resourcePath ) . isDirectory ( ) ; const componentName = isDirectory ? componentUtil . moduleFromDirectory ( resourcePath , isSFDX ) : componentUtil . moduleFromFile ( resourcePath , isSFDX ) ; if ( ! componentName ) { const message = nls . localize ( 'force_lightning_lwc_preview_unsupported' , resourcePath ) ; showError ( new Error ( message ) , logName , commandName ) ; return ; } await executePreview ( startTime , componentName , resourcePath ) ; } param startTime param componentName param resourcePath async function executePreview ( startTime , componentName , resourcePath ) { const commandCancelledMessage = nls . localize ( 'force_lightning_lwc_operation_cancelled' ) ; const platformSelection = await selectPlatform ( ) ; if ( ! platformSelection ) { vscode . window . showWarningMessage ( commandCancelledMessage ) ; return ; } if ( platformSelection . id === PreviewPlatformType . Desktop ) { await startServer ( true , componentName , startTime ) ; return ; } let targetDevice ; try { const targetName = await selectTargetDevice ( platformSelection ) ; if ( targetName === undefined ) { vscode . window . showInformationMessage ( commandCancelledMessage ) ; return ; } else { targetDevice = targetName ; } } catch { return ; } const projectRootDir = getProjectRootDirectory ( resourcePath ) ; const configFilePath = projectRootDir && path . join ( projectRootDir , 'mobile-apps.json' ) ; const targetApp = await selectTargetApp ( platformSelection , configFilePath ) ; if ( targetApp === undefined ) { vscode . window . showInformationMessage ( commandCancelledMessage ) ; return ; } await startServer ( false , componentName , startTime ) ; await executeMobilePreview ( platformSelection , targetDevice , targetApp , projectRootDir , configFilePath , componentName , startTime ) ; } param isDesktop param componentName param startTime async function startServer ( isDesktop , componentName , startTime ) { if ( ! DevServerService . instance . isServerHandlerRegistered ( ) ) { console . log ( `${ logName }: server was not running, starting...` ) ; const preconditionChecker = new SfdxWorkspaceChecker ( ) ; const parameterGatherer = new EmptyParametersGatherer ( ) ; const executor = new ForceLightningLwcStartExecutor ( { openBrowser : isDesktop , componentName } ) ; const commandlet = new SfdxCommandlet ( preconditionChecker , parameterGatherer , executor ) ; await commandlet . run ( ) ; telemetryService . sendCommandEvent ( logName , startTime ) ; } else if ( isDesktop ) { try { const fullUrl = DevServerService . instance . getComponentPreviewUrl ( componentName ) ; await openBrowser ( fullUrl ) ; telemetryService . sendCommandEvent ( logName , startTime ) ; } catch ( e ) { showError ( e , logName , commandName ) ; } } } returns async function selectPlatform ( ) { const platformSelection = await vscode . window . showQuickPick ( platformOptions , { placeHolder : nls . localize ( 'force_lightning_lwc_platform_selection' ) } ) ; return platformSelection ; } param platformSelection returns async function selectTargetDevice ( platformSelection ) { const isAndroid = platformSelection . id === PreviewPlatformType . Android ; const lastTarget = PreviewService . instance . getRememberedDevice ( platformSelection . platformName ) ; let target = platformSelection . defaultTargetName ; let createDevicePlaceholderText = isAndroid ? nls . localize ( 'force_lightning_lwc_android_target_default' ) : nls . localize ( 'force_lightning_lwc_ios_target_default' ) ; if ( PreviewService . instance . isRememberedDeviceEnabled ( ) && lastTarget ) { const message = isAndroid ? 'force_lightning_lwc_android_target_remembered' : 'force_lightning_lwc_ios_target_remembered' ; createDevicePlaceholderText = nls . localize ( message , lastTarget ) ; target = lastTarget ; } const deviceListOutput = new CommandOutput ( ) ; const deviceListCommand = new SfdxCommandBuilder ( ) . withArg ( sfdxDeviceListCommand ) . withFlag ( '-p' , platformSelection . platformName ) . withJson ( ) . build ( ) ; let deviceListExecutionExitCode ; const deviceListCancellationTokenSource = new vscode . CancellationTokenSource ( ) ; const deviceListCancellationToken = deviceListCancellationTokenSource . token ; const deviceListExecutor = new CliCommandExecutor ( deviceListCommand , { } ) ; const deviceListExecution = deviceListExecutor . execute ( deviceListCancellationToken ) ; deviceListExecution . processExitSubject . subscribe ( exitCode => { deviceListExecutionExitCode = exitCode ; } ) ; const items = [ ] ; const createNewDeviceItem = { label : nls . localize ( 'force_lightning_lwc_preview_create_virtual_device_label' ) , detail : nls . localize ( 'force_lightning_lwc_preview_create_virtual_device_detail' ) , name : nls . localize ( 'force_lightning_lwc_preview_create_virtual_device_label' ) } ; let targetName ; try { const result = await deviceListOutput . getCmdResult ( deviceListExecution ) ; const jsonString = result . substring ( result . indexOf ( '{' ) ) ; if ( isAndroid ) { const devices = JSON . parse ( jsonString ) . result as AndroidVirtualDevice [ ] ; devices . forEach ( device => { const label = device . displayName ; const detail = `${ device . target }, ${ device . api }` ; const name = device . name ; items . push ( { label , detail , name } ) ; } ) ; } else { const devices = JSON . parse ( jsonString ) . result as IOSSimulatorDevice [ ] ; devices . forEach ( device => { const label = device . name ; const detail = device . runtimeId ; const name = device . name ; items . push ( { label , detail , name } ) ; } ) ; } } catch ( e ) { const error = String ( e ) || '' ; if ( deviceListExecutionExitCode === 127 || error . includes ( 'not a sfdx command' ) ) { showError ( new Error ( nls . localize ( 'force_lightning_lwc_no_mobile_plugin' ) ) , logName , commandName ) ; throw e ; } } let selectedItem ; if ( items . length > 0 ) { items . unshift ( createNewDeviceItem ) ; selectedItem = await vscode . window . showQuickPick ( items , { placeHolder : nls . localize ( 'force_lightning_lwc_preview_select_virtual_device' ) } ) ; if ( selectedItem === undefined ) { return undefined ; } else { targetName = selectedItem . name ; } } if ( targetName === undefined || selectedItem === createNewDeviceItem ) { targetName = await vscode . window . showInputBox ( { placeHolder : createDevicePlaceholderText } ) ; if ( targetName === undefined ) { return undefined ; } } if ( targetName && targetName !== '' ) { PreviewService . instance . updateRememberedDevice ( platformSelection . platformName , targetName ) ; target = targetName ; } return target ; } param platformSelection param configFile returns async function selectTargetApp ( platformSelection , configFile ) { let targetApp = 'browser' ; const items = [ ] ; const browserItem = { label : nls . localize ( 'force_lightning_lwc_browserapp_label' ) , detail : nls . localize ( 'force_lightning_lwc_browserapp_description' ) } ; if ( configFile === undefined || fs . existsSync ( configFile ) === false ) { return targetApp ; } try { const fileContent = fs . readFileSync ( configFile , 'utf8' ) ; const json = JSON . parse ( fileContent ) ; const appDefinitionsForSelectedPlatform = platformSelection . id === PreviewPlatformType . Android ? json . apps . android : json . apps . ios ; const apps = Array . from < any > ( appDefinitionsForSelectedPlatform ) ; apps . forEach ( app => { const label = app . name ; const detail = app . id ; items . push ( { label , detail } ) ; } ) ; } catch { } if ( items . length > 0 ) { items . unshift ( browserItem ) ; const selectedItem = await vscode . window . showQuickPick ( items , { placeHolder : nls . localize ( 'force_lightning_lwc_preview_select_target_app' ) } ) ; if ( selectedItem ) { if ( selectedItem !== browserItem ) { targetApp = selectedItem . detail ; } } else { targetApp = undefined ; } } return targetApp ; } param platformSelection param targetDevice param targetApp param projectDir param configFile param componentName param startTime async function executeMobilePreview ( platformSelection , targetDevice , targetApp , projectDir , configFile , componentName , startTime ) { const isAndroid = platformSelection . id === PreviewPlatformType . Android ; let commandBuilder = new SfdxCommandBuilder ( ) . withDescription ( commandName ) . withArg ( sfdxMobilePreviewCommand ) . withFlag ( '-p' , platformSelection . platformName ) . withFlag ( '-t' , targetDevice ) . withFlag ( '-n' , componentName ) . withFlag ( '-a' , targetApp ) ; if ( projectDir ) { commandBuilder = commandBuilder . withFlag ( '-d' , projectDir ) ; } if ( configFile && targetApp !== 'browser' ) { commandBuilder = commandBuilder . withFlag ( '-f' , configFile ) ; } const previewCommand = commandBuilder . withFlag ( '--loglevel' , PreviewService . instance . getLogLevel ( ) ) . build ( ) ; const previewExecutor = new CliCommandExecutor ( previewCommand , { env : { SFDX_JSON_TO_STDOUT : 'true' } } ) ; const previewCancellationTokenSource = new vscode . CancellationTokenSource ( ) ; const previewCancellationToken = previewCancellationTokenSource . token ; const previewExecution = previewExecutor . execute ( previewCancellationToken ) ; telemetryService . sendCommandEvent ( logName , startTime ) ; channelService . streamCommandOutput ( previewExecution ) ; channelService . showChannelOutput ( ) ; previewExecution . processExitSubject . subscribe ( async ( exitCode ) => { if ( exitCode !== 0 ) { const message = isAndroid ? nls . localize ( 'force_lightning_lwc_android_failure' , targetDevice ) : nls . localize ( 'force_lightning_lwc_ios_failure' , targetDevice ) ; showError ( new Error ( message ) , logName , commandName ) ; } else if ( ! isAndroid ) { notificationService . showSuccessfulExecution ( previewExecution . command . toString ( ) , channelService ) . catch ( ) ; vscode . window . showInformationMessage ( nls . localize ( 'force_lightning_lwc_ios_start' , targetDevice ) ) ; } } ) ; if ( isAndroid ) { previewExecution . stdoutSubject . subscribe ( async ( data ) => { if ( data && data . toString ( ) . includes ( androidSuccessString ) ) { notificationService . showSuccessfulExecution ( previewExecution . command . toString ( ) , channelService ) . catch ( ) ; vscode . window . showInformationMessage ( nls . localize ( 'force_lightning_lwc_android_start' , targetDevice ) ) ; } } ) ; } } param startPath returns export function getProjectRootDirectory ( startPath ) { if ( ! fs . existsSync ( startPath ) ) { return undefined ; } const searchingForFile = 'sfdx-project.json' ; let dir = fs . lstatSync ( startPath ) . isDirectory ( ) ? startPath : path . dirname ( startPath ) ; while ( dir ) { const fileName = path . join ( dir , searchingForFile ) ; if ( fs . existsSync ( fileName ) ) { return dir ; } else { dir = directoryLevelUp ( dir ) ; } } return undefined ; } param directory returns export function directoryLevelUp ( directory ) { const levelUp = path . dirname ( directory ) ; if ( levelUp === directory ) { return undefined ; } return levelUp ; }