77
88import path from 'node:path' ;
99import * as readline from 'node:readline' ;
10- import { Logger , Messages } from '@salesforce/core' ;
10+ import { Logger , Messages , SfProject } from '@salesforce/core' ;
1111import {
1212 AndroidAppPreviewConfig ,
1313 AndroidVirtualDevice ,
@@ -21,7 +21,6 @@ import chalk from 'chalk';
2121import { OrgUtils } from '../../../shared/orgUtils.js' ;
2222import { startLWCServer } from '../../../lwc-dev-server/index.js' ;
2323import { PreviewUtils } from '../../../shared/previewUtils.js' ;
24- import { LwcDevServerUtils } from '../../../shared/lwcDevServerUtils.js' ;
2524
2625Messages . importMessagesDirectoryFromMetaUrl ( import . meta. url ) ;
2726const messages = Messages . loadMessages ( '@salesforce/plugin-lightning-dev' , 'lightning.preview.app' ) ;
@@ -37,6 +36,8 @@ export const androidSalesforceAppPreviewConfig = {
3736 activity : 'com.salesforce.chatter.Chatter' ,
3837} as AndroidAppPreviewConfig ;
3938
39+ const maxInt32 = 2_147_483_647 ; // maximum 32-bit signed integer value
40+
4041export default class LightningPreviewApp extends SfCommand < void > {
4142 public static readonly summary = messages . getMessage ( 'summary' ) ;
4243 public static readonly description = messages . getMessage ( 'description' ) ;
@@ -62,15 +63,44 @@ export default class LightningPreviewApp extends SfCommand<void> {
6263 } ) ,
6364 } ;
6465
65- public static async waitForUserToInstallCert (
66+ private static async waitForKeyPress ( ) : Promise < void > {
67+ return new Promise ( ( resolve ) => {
68+ const rl = readline . createInterface ( {
69+ input : process . stdin ,
70+ output : process . stdout ,
71+ } ) ;
72+
73+ // eslint-disable-next-line no-console
74+ console . log ( `\n${ messages . getMessage ( 'certificate.waiting' ) } \n` ) ;
75+
76+ process . stdin . setRawMode ( true ) ;
77+ process . stdin . resume ( ) ;
78+ process . stdin . once ( 'data' , ( ) => {
79+ process . stdin . setRawMode ( false ) ;
80+ process . stdin . pause ( ) ;
81+ rl . close ( ) ;
82+ resolve ( ) ;
83+ } ) ;
84+ } ) ;
85+ }
86+
87+ public async waitForUserToInstallCert (
6688 platform : Platform . ios | Platform . android ,
6789 device : IOSSimulatorDevice | AndroidVirtualDevice ,
6890 certFilePath : string
6991 ) : Promise < void > {
70- let attention = `\n${ messages . getMessage ( 'certificate.attention' ) } ` ;
71- attention = chalk . red ( attention ) ;
7292 // eslint-disable-next-line no-console
73- console . log ( attention ) ;
93+ console . log ( `\n${ messages . getMessage ( 'certificate.installation.notice' ) } ` ) ;
94+
95+ const skipInstall = await this . confirm ( {
96+ message : messages . getMessage ( 'certificate.installation.skip.message' ) ,
97+ defaultAnswer : true ,
98+ ms : maxInt32 , // simulate no timeout and wait for user to answer
99+ } ) ;
100+
101+ if ( skipInstall ) {
102+ return ;
103+ }
74104
75105 let installationSteps = '' ;
76106 if ( platform === Platform . ios ) {
@@ -111,34 +141,6 @@ export default class LightningPreviewApp extends SfCommand<void> {
111141 return LightningPreviewApp . waitForKeyPress ( ) ;
112142 }
113143
114- private static async waitForKeyPress ( ) : Promise < void > {
115- return new Promise ( ( resolve ) => {
116- // Emit keypress events on stdin
117- readline . emitKeypressEvents ( process . stdin ) ;
118- // Set stdin to raw mode
119- if ( process . stdin . isTTY ) {
120- process . stdin . setRawMode ( true ) ;
121- }
122-
123- // eslint-disable-next-line no-console
124- console . log ( `\n${ messages . getMessage ( 'certificate.waiting' ) } \n` ) ;
125-
126- // Function to handle key press
127- function onKeyPress ( ) : void {
128- // Restore stdin settings
129- if ( process . stdin . isTTY ) {
130- process . stdin . setRawMode ( false ) ;
131- }
132- process . stdin . removeListener ( 'keypress' , onKeyPress ) ;
133- process . stdin . pause ( ) ;
134- resolve ( ) ;
135- }
136-
137- // Add keypress listener
138- process . stdin . on ( 'keypress' , onKeyPress ) ;
139- } ) ;
140- }
141-
142144 public async run ( ) : Promise < void > {
143145 const { flags } = await this . parse ( LightningPreviewApp ) ;
144146 const logger = await Logger . child ( this . ctor . name ) ;
@@ -148,13 +150,12 @@ export default class LightningPreviewApp extends SfCommand<void> {
148150 const targetOrg = flags [ 'target-org' ] ;
149151 const deviceId = flags [ 'device-id' ] ;
150152
151- logger . debug ( 'Determining Local Dev Server url' ) ;
152- // todo: figure out how to make the port dynamic instead of hard-coded value here
153- const ldpServerUrl = PreviewUtils . generateWebSocketUrlForLocalDevServer (
154- platform ,
155- `${ await LwcDevServerUtils . getLocalDevServerPort ( ) } `
156- ) ;
157- logger . debug ( `Local Dev Server url is ${ ldpServerUrl } ` ) ;
153+ let sfdxProjectRootPath = '' ;
154+ try {
155+ sfdxProjectRootPath = await SfProject . resolveProjectPath ( ) ;
156+ } catch ( error ) {
157+ return Promise . reject ( new Error ( messages . getMessage ( 'error.no-project' , [ ( error as Error ) ?. message ?? '' ] ) ) ) ;
158+ }
158159
159160 let appId : string | undefined ;
160161 if ( appName ) {
@@ -170,16 +171,39 @@ export default class LightningPreviewApp extends SfCommand<void> {
170171 logger . debug ( `App Id is ${ appId } ` ) ;
171172 }
172173
174+ logger . debug ( 'Determining the next available port for Local Dev Server' ) ;
175+ const serverPort = await PreviewUtils . getNextAvailablePort ( ) ;
176+ logger . debug ( `Next available port is ${ serverPort } ` ) ;
177+
178+ logger . debug ( 'Determining Local Dev Server url' ) ;
179+ const ldpServerUrl = PreviewUtils . generateWebSocketUrlForLocalDevServer ( platform , serverPort ) ;
180+ logger . debug ( `Local Dev Server url is ${ ldpServerUrl } ` ) ;
181+
173182 if ( platform === Platform . desktop ) {
174- await this . desktopPreview ( ldpServerUrl , appId , logger ) ;
183+ await this . desktopPreview ( sfdxProjectRootPath , serverPort , ldpServerUrl , appId , logger ) ;
175184 } else {
176- await this . mobilePreview ( platform , ldpServerUrl , appName , appId , deviceId , logger ) ;
185+ await this . mobilePreview (
186+ platform ,
187+ sfdxProjectRootPath ,
188+ serverPort ,
189+ ldpServerUrl ,
190+ appName ,
191+ appId ,
192+ deviceId ,
193+ logger
194+ ) ;
177195 }
178196 }
179197
180- private async desktopPreview ( ldpServerUrl : string , appId ?: string , logger ?: Logger ) : Promise < void > {
198+ private async desktopPreview (
199+ sfdxProjectRootPath : string ,
200+ serverPort : number ,
201+ ldpServerUrl : string ,
202+ appId : string | undefined ,
203+ logger : Logger
204+ ) : Promise < void > {
181205 if ( ! appId ) {
182- logger ? .debug ( 'No Lightning Experience application name provided.... using the default app instead.' ) ;
206+ logger . debug ( 'No Lightning Experience application name provided.... using the default app instead.' ) ;
183207 }
184208
185209 // There are various ways to pass in a target org (as an alias, as a username, etc).
@@ -202,46 +226,55 @@ export default class LightningPreviewApp extends SfCommand<void> {
202226 targetOrg = this . argv [ idx + 1 ] ;
203227 }
204228
229+ const protocol = new URL ( ldpServerUrl ) . protocol . replace ( ':' , '' ) . toLowerCase ( ) ;
230+ if ( protocol === 'wss' ) {
231+ this . log ( `\n${ messages . getMessage ( 'trust.local.dev.server' ) } ` ) ;
232+ }
233+
205234 const launchArguments = PreviewUtils . generateDesktopPreviewLaunchArguments ( ldpServerUrl , appId , targetOrg ) ;
206235
207236 // Start the LWC Dev Server
208- await startLWCServer ( process . cwd ( ) , logger ? logger : await Logger . child ( this . ctor . name ) ) ;
237+ await startLWCServer ( logger , sfdxProjectRootPath , serverPort , protocol ) ;
209238
239+ // Open the browser and navigate to the right page
210240 await this . config . runCommand ( 'org:open' , launchArguments ) ;
211241 }
212242
213243 private async mobilePreview (
214244 platform : Platform . ios | Platform . android ,
245+ sfdxProjectRootPath : string ,
246+ serverPort : number ,
215247 ldpServerUrl : string ,
216- appName ? : string ,
217- appId ? : string ,
218- deviceId ? : string ,
219- logger ? : Logger
248+ appName : string | undefined ,
249+ appId : string | undefined ,
250+ deviceId : string | undefined ,
251+ logger : Logger
220252 ) : Promise < void > {
221253 try {
222- // 1. Verify that user environment is set up for mobile (i.e. has right tooling)
254+ // Verify that user environment is set up for mobile (i.e. has right tooling)
223255 await this . verifyMobileRequirements ( platform , logger ) ;
224256
225- // 2. Fetch the target device
257+ // Fetch the target device
226258 const device = await PreviewUtils . getMobileDevice ( platform , deviceId , logger ) ;
227259 if ( ! device ) {
228260 throw new Error ( messages . getMessage ( 'error.device.notfound' , [ deviceId ?? '' ] ) ) ;
229261 }
230262
231- // 3. Boot the device if not already booted
263+ // Boot the device if not already booted
232264 this . spinner . start ( messages . getMessage ( 'spinner.device.boot' , [ device . toString ( ) ] ) ) ;
233265 const resolvedDeviceId = platform === Platform . ios ? ( device as IOSSimulatorDevice ) . udid : device . name ;
234266 const emulatorPort = await PreviewUtils . bootMobileDevice ( platform , resolvedDeviceId , logger ) ;
235267 this . spinner . stop ( ) ;
236268
237- // 4. Generate self-signed certificate and wait for user to install it
238- // TODO: update the save location to be the same as server config file path
269+ // Configure certificates for dev server secure connection
239270 this . spinner . start ( messages . getMessage ( 'spinner.cert.gen' ) ) ;
240- const certFilePath = PreviewUtils . generateSelfSignedCert ( platform , '~/Desktop/cert' ) ;
271+ const { certData , certFilePath } = await PreviewUtils . generateSelfSignedCert ( platform , sfdxProjectRootPath ) ;
241272 this . spinner . stop ( ) ;
242- await LightningPreviewApp . waitForUserToInstallCert ( platform , device , certFilePath ) ;
243273
244- // 5. Check if Salesforce Mobile App is installed on the device
274+ // Show message and wait for user to install the certificate on their device
275+ await this . waitForUserToInstallCert ( platform , device , certFilePath ) ;
276+
277+ // Check if Salesforce Mobile App is installed on the device
245278 const appConfig = platform === Platform . ios ? iOSSalesforceAppPreviewConfig : androidSalesforceAppPreviewConfig ;
246279 const appInstalled = await PreviewUtils . verifyMobileAppInstalled (
247280 platform ,
@@ -251,10 +284,9 @@ export default class LightningPreviewApp extends SfCommand<void> {
251284 logger
252285 ) ;
253286
254- // 6. If Salesforce Mobile App is not installed, download and install it
287+ // If Salesforce Mobile App is not installed, download and install it
255288 let bundlePath : string | undefined ;
256289 if ( ! appInstalled ) {
257- const maxInt32 = 2_147_483_647 ; // maximum 32-bit signed integer value
258290 const proceedWithDownload = await this . confirm ( {
259291 message : messages . getMessage ( 'mobileapp.download' , [ appConfig . name ] ) ,
260292 defaultAnswer : false ,
@@ -285,8 +317,10 @@ export default class LightningPreviewApp extends SfCommand<void> {
285317 }
286318
287319 // Start the LWC Dev Server
288- await startLWCServer ( process . cwd ( ) , logger ? logger : await Logger . child ( this . ctor . name ) ) ;
289- // 7. Launch the native app for previewing (launchMobileApp will show its own spinner)
320+ const protocol = new URL ( ldpServerUrl ) . protocol . replace ( ':' , '' ) . toLowerCase ( ) ;
321+ await startLWCServer ( logger , sfdxProjectRootPath , serverPort , protocol , certData ) ;
322+
323+ // Launch the native app for previewing (launchMobileApp will show its own spinner)
290324 // eslint-disable-next-line camelcase
291325 appConfig . launch_arguments = PreviewUtils . generateMobileAppPreviewLaunchArguments ( ldpServerUrl , appName , appId ) ;
292326 await PreviewUtils . launchMobileApp ( platform , appConfig , resolvedDeviceId , emulatorPort , bundlePath , logger ) ;
@@ -305,13 +339,13 @@ export default class LightningPreviewApp extends SfCommand<void> {
305339 * @param platform A mobile platform (iOS or Android)
306340 * @param logger An optional logger to be used for logging
307341 */
308- private async verifyMobileRequirements ( platform : Platform . ios | Platform . android , logger ? : Logger ) : Promise < void > {
309- logger ? .debug ( `Verifying environment meets requirements for previewing on ${ platform } ` ) ;
342+ private async verifyMobileRequirements ( platform : Platform . ios | Platform . android , logger : Logger ) : Promise < void > {
343+ logger . debug ( `Verifying environment meets requirements for previewing on ${ platform } ` ) ;
310344
311345 const setupCommand = new LwcDevMobileCoreSetup ( [ '-p' , platform ] , this . config ) ;
312346 await setupCommand . init ( ) ;
313347 await setupCommand . run ( ) ;
314348
315- logger ? .debug ( 'Requirements are met' ) ; // if we make it here then all is good
349+ logger . debug ( 'Requirements are met' ) ; // if we make it here then all is good
316350 }
317351}
0 commit comments