1313import fs from 'node:fs' ;
1414import os from 'node:os' ;
1515import path from 'node:path' ;
16- import { Connection , Logger , Messages } from '@salesforce/core' ;
16+ import { Connection , Logger , Messages , Org } from '@salesforce/core' ;
1717import {
1818 AndroidDeviceManager ,
1919 AppleDeviceManager ,
@@ -33,8 +33,15 @@ import { PromptUtils } from './promptUtils.js';
3333
3434Messages . importMessagesDirectoryFromMetaUrl ( import . meta. url ) ;
3535const messages = Messages . loadMessages ( '@salesforce/plugin-lightning-dev' , 'lightning.dev.app' ) ;
36+ const sharedMessages = Messages . loadMessages ( '@salesforce/plugin-lightning-dev' , 'shared.utils' ) ;
3637const DevPreviewAuraMode = 'DEVPREVIEW' ;
3738
39+ export type PreviewConnection = {
40+ connection : Connection ;
41+ ldpServerId : string ;
42+ ldpServerToken : string ;
43+ } ;
44+
3845export class PreviewUtils {
3946 public static generateWebSocketUrlForLocalDevServer (
4047 platform : string ,
@@ -139,6 +146,37 @@ export class PreviewUtils {
139146 }
140147 }
141148
149+ /**
150+ * Extracts the target org from command line arguments.
151+ *
152+ * There are various ways to pass in a target org (as an alias, as a username, etc).
153+ * We could have LightningPreviewApp parse its --target-org flag which will be resolved
154+ * to an Org object (see https://github.com/forcedotcom/sfdx-core/blob/main/src/org/org.ts)
155+ * then write a bunch of code to look at this Org object to try to determine whether
156+ * it was initialized using Alias, Username, etc. and get a string representation of the
157+ * org to be forwarded to OrgOpenCommand.
158+ *
159+ * Or we could simply look at the raw arguments passed to the LightningPreviewApp command,
160+ * find the raw value for --target-org flag and forward that raw value to OrgOpenCommand.
161+ * The OrgOpenCommand will then parse the raw value automatically. If the value is
162+ * valid then OrgOpenCommand will consume it and continue. And if the value is invalid then
163+ * OrgOpenCommand simply throws an error which will get bubbled up to LightningPreviewApp.
164+ *
165+ * Here we've chosen the second approach.
166+ *
167+ * @param args - Array of command line arguments
168+ * @returns The target org identifier if found, undefined otherwise
169+ */
170+ public static getTargetOrgFromArguments ( args : string [ ] ) : string | undefined {
171+ const idx = args . findIndex ( ( item ) => item . toLowerCase ( ) === '-o' || item . toLowerCase ( ) === '--target-org' ) ;
172+ let targetOrg : string | undefined ;
173+ if ( idx >= 0 && idx < args . length - 1 ) {
174+ targetOrg = args [ idx + 1 ] ;
175+ }
176+
177+ return targetOrg ;
178+ }
179+
142180 /**
143181 * Generates the proper set of arguments to be used for launching desktop browser and navigating to the right location.
144182 *
@@ -176,6 +214,38 @@ export class PreviewUtils {
176214 return launchArguments ;
177215 }
178216
217+ /**
218+ * Generates the proper set of arguments to be used for launching a component preview in the browser.
219+ *
220+ * @param ldpServerUrl The URL for the local dev server
221+ * @param ldpServerId Record ID for the identity token
222+ * @param componentName The name of the component to preview
223+ * @param targetOrg An optional org id
224+ * @returns Array of arguments to be used by Org:Open command for launching the component preview
225+ */
226+ public static generateComponentPreviewLaunchArguments (
227+ ldpServerUrl : string ,
228+ ldpServerId : string ,
229+ componentName ?: string ,
230+ targetOrg ?: string
231+ ) : string [ ] {
232+ // TODO: vanity application target
233+ let appPath = `lwr/application/ai/${ encodeURIComponent (
234+ 'localdev%2Fpreview'
235+ ) } ?ldpServerUrl=${ ldpServerUrl } &ldpServerId=${ ldpServerId } `;
236+ if ( componentName ) {
237+ appPath += `&specifier=c/${ componentName } ` ;
238+ }
239+
240+ const launchArguments = [ '--path' , appPath ] ;
241+
242+ if ( targetOrg ) {
243+ launchArguments . push ( '--target-org' , targetOrg ) ;
244+ }
245+
246+ return launchArguments ;
247+ }
248+
179249 /**
180250 * Generates the proper set of arguments to be used for launching a mobile app with custom launch arguments.
181251 *
@@ -324,6 +394,34 @@ export class PreviewUtils {
324394 } ) ;
325395 }
326396
397+ public static async initializePreviewConnection ( targetOrg : Org ) : Promise < PreviewConnection > {
398+ const connection = targetOrg . getConnection ( undefined ) ;
399+ const username = connection . getUsername ( ) ;
400+ if ( ! username ) {
401+ return Promise . reject ( new Error ( messages . getMessage ( 'error.username' ) ) ) ;
402+ }
403+
404+ const localDevEnabled = await OrgUtils . isLocalDevEnabled ( connection ) ;
405+ if ( ! localDevEnabled ) {
406+ return Promise . reject ( new Error ( sharedMessages . getMessage ( 'error.localdev.not.enabled' ) ) ) ;
407+ }
408+
409+ OrgUtils . ensureMatchingAPIVersion ( connection ) ;
410+
411+ const appServerIdentity = await PreviewUtils . getOrCreateAppServerIdentity ( connection ) ;
412+ const ldpServerToken = appServerIdentity . identityToken ;
413+ const ldpServerId = appServerIdentity . usernameToServerEntityIdMap [ username ] ;
414+ if ( ! ldpServerId ) {
415+ return Promise . reject ( new Error ( messages . getMessage ( 'error.identitydata.entityid' ) ) ) ;
416+ }
417+
418+ return {
419+ connection,
420+ ldpServerId,
421+ ldpServerToken,
422+ } ;
423+ }
424+
327425 public static async getOrCreateAppServerIdentity ( connection : Connection ) : Promise < LocalWebServerIdentityData > {
328426 const username = connection . getUsername ( ) ! ;
329427
0 commit comments