77
88import process from 'node:process' ;
99import { LWCServer , ServerConfig , startLwcDevServer , Workspace } from '@lwc/lwc-dev-server' ;
10- import { Lifecycle , Logger , SfProject } from '@salesforce/core' ;
11- import { SSLCertificateData } from '@salesforce/lwc-dev-mobile-core' ;
10+ import { Lifecycle , Logger , SfProject , AuthInfo , Connection } from '@salesforce/core' ;
11+ import { SSLCertificateData , Platform } from '@salesforce/lwc-dev-mobile-core' ;
1212import { glob } from 'glob' ;
1313import {
1414 ConfigUtils ,
1515 LOCAL_DEV_SERVER_DEFAULT_HTTP_PORT ,
1616 LOCAL_DEV_SERVER_DEFAULT_WORKSPACE ,
1717} from '../shared/configUtils.js' ;
18+ import { PreviewUtils } from '../shared/previewUtils.js' ;
1819
1920async function createLWCServerConfig (
2021 rootDir : string ,
@@ -27,7 +28,7 @@ async function createLWCServerConfig(
2728 const project = await SfProject . resolve ( ) ;
2829 const packageDirs = project . getPackageDirectories ( ) ;
2930 const projectJson = await project . resolveProjectConfig ( ) ;
30- const { namespace } = projectJson ;
31+ const { namespace } = projectJson as { namespace ?: string } ;
3132
3233 // e.g. lwc folders in force-app/main/default/lwc, package-dir/lwc
3334 const namespacePaths = (
@@ -73,6 +74,9 @@ export async function startLWCServer(
7374 certData ?: SSLCertificateData ,
7475 workspace ?: Workspace
7576) : Promise < LWCServer > {
77+ // Validate JWT authentication before starting the server
78+ await ensureJwtAuth ( clientType ) ;
79+
7680 const config = await createLWCServerConfig ( rootDir , token , clientType , serverPorts , certData , workspace ) ;
7781
7882 logger . trace ( `Starting LWC Dev Server with config: ${ JSON . stringify ( config ) } ` ) ;
@@ -94,3 +98,208 @@ export async function startLWCServer(
9498
9599 return lwcDevServer ;
96100}
101+
102+ /**
103+ * Helper function to ensure JWT authentication is valid
104+ */
105+ async function ensureJwtAuth ( username : string ) : Promise < AuthInfo > {
106+ try {
107+ // Create AuthInfo - this will throw if authentication is invalid
108+ const authInfo = await AuthInfo . create ( { username } ) ;
109+
110+ // Verify the AuthInfo has valid credentials
111+ const authUsername = authInfo . getUsername ( ) ;
112+ if ( ! authUsername ) {
113+ throw new Error ( 'AuthInfo created but username is not available' ) ;
114+ }
115+
116+ return authInfo ;
117+ } catch ( e ) {
118+ const errorMessage = ( e as Error ) . message ;
119+ // Provide more helpful error messages based on common authentication issues
120+ if ( errorMessage . includes ( 'No authorization information found' ) ) {
121+ throw new Error (
122+ `JWT authentication not found for user ${ username } . Please run 'sf org login jwt' or 'sf org login web' first.`
123+ ) ;
124+ } else if (
125+ errorMessage . includes ( 'expired' ) ||
126+ errorMessage . includes ( 'Invalid JWT token' ) ||
127+ errorMessage . includes ( 'invalid signature' )
128+ ) {
129+ throw new Error (
130+ `JWT authentication expired or invalid for user ${ username } . Please re-authenticate using 'sf org login jwt' or 'sf org login web'.`
131+ ) ;
132+ } else {
133+ throw new Error ( `JWT authentication not found or invalid for user ${ username } : ${ errorMessage } ` ) ;
134+ }
135+ }
136+ }
137+
138+ /**
139+ * Configuration for starting the local dev server programmatically
140+ */
141+ export type LocalDevServerConfig = {
142+ /** Target org connection */
143+ targetOrg : unknown ;
144+ /** Component name to preview */
145+ componentName ?: string ;
146+ /** Platform for preview (defaults to desktop) */
147+ platform ?: Platform ;
148+ /** Custom port configuration */
149+ ports ?: {
150+ httpPort ?: number ;
151+ httpsPort ?: number ;
152+ } ;
153+ /** Logger instance */
154+ logger ?: Logger ;
155+ } ;
156+
157+ /**
158+ * Result from starting the local dev server
159+ */
160+ export type LocalDevServerResult = {
161+ /** Local dev server URL */
162+ url : string ;
163+ /** Server ID for authentication */
164+ serverId : string ;
165+ /** Authentication token */
166+ token : string ;
167+ /** Server ports */
168+ ports : {
169+ httpPort : number ;
170+ httpsPort : number ;
171+ } ;
172+ /** Server process for cleanup */
173+ process ?: LWCServer ;
174+ } ;
175+
176+ /**
177+ * Programmatic API for starting the local dev server
178+ * This can be used to start the server without CLI
179+ */
180+ export class LocalDevServerManager {
181+ private static instance : LocalDevServerManager ;
182+ private activeServers : Map < string , LocalDevServerResult > = new Map ( ) ;
183+
184+ private constructor ( ) { }
185+
186+ public static getInstance ( ) : LocalDevServerManager {
187+ if ( ! LocalDevServerManager . instance ) {
188+ LocalDevServerManager . instance = new LocalDevServerManager ( ) ;
189+ }
190+ return LocalDevServerManager . instance ;
191+ }
192+
193+ /**
194+ * Start the local dev server programmatically
195+ *
196+ * @param config Configuration for the server
197+ * @returns Promise with server details including URL for iframing
198+ */
199+ public async startServer ( config : LocalDevServerConfig ) : Promise < LocalDevServerResult > {
200+ const logger = config . logger ?? ( await Logger . child ( 'LocalDevServerManager' ) ) ;
201+ const platform = config . platform ?? Platform . desktop ;
202+
203+ if ( typeof config . targetOrg !== 'string' ) {
204+ const error = new Error ( 'targetOrg must be a valid username string.' ) ;
205+ logger . error ( 'Invalid targetOrg parameter' , { targetOrg : config . targetOrg } ) ;
206+ throw error ;
207+ }
208+
209+ logger . info ( 'Starting Local Dev Server' , { platform : platform . toString ( ) , targetOrg : config . targetOrg } ) ;
210+
211+ let sfdxProjectRootPath = '' ;
212+ try {
213+ sfdxProjectRootPath = await SfProject . resolveProjectPath ( ) ;
214+ logger . debug ( 'SFDX project path resolved' , { path : sfdxProjectRootPath } ) ;
215+ } catch ( error ) {
216+ const errorMessage = `No SFDX project found: ${ ( error as Error ) ?. message || '' } ` ;
217+ logger . error ( 'Failed to resolve SFDX project path' , { error : errorMessage } ) ;
218+ throw new Error ( errorMessage ) ;
219+ }
220+
221+ try {
222+ logger . debug ( 'Validating JWT authentication' , { targetOrg : config . targetOrg } ) ;
223+ const authInfo = await ensureJwtAuth ( config . targetOrg ) ;
224+ const connection = await Connection . create ( { authInfo } ) ;
225+
226+ const ldpServerToken = connection . getConnectionOptions ( ) . accessToken ;
227+ if ( ! ldpServerToken ) {
228+ const error = new Error (
229+ 'Unable to retrieve access token from targetOrg. Ensure the org is authenticated and has a valid session.'
230+ ) ;
231+ logger . error ( 'Access token retrieval failed' , { targetOrg : config . targetOrg } ) ;
232+ throw error ;
233+ }
234+
235+ const ldpServerId = authInfo . getUsername ( ) ; // Using username as server ID
236+ logger . debug ( 'Authentication successful' , { serverId : ldpServerId } ) ;
237+
238+ const serverPorts = config . ports
239+ ? { httpPort : config . ports . httpPort ?? 3333 , httpsPort : config . ports . httpsPort ?? 3334 }
240+ : await PreviewUtils . getNextAvailablePorts ( ) ;
241+
242+ logger . debug ( 'Server ports configured' , { ports : serverPorts } ) ;
243+
244+ const ldpServerUrl = PreviewUtils . generateWebSocketUrlForLocalDevServer ( platform , serverPorts , logger ) ;
245+
246+ logger . info ( 'Starting LWC Dev Server process' , { ports : serverPorts } ) ;
247+ const serverProcess = await startLWCServer (
248+ logger ,
249+ sfdxProjectRootPath ,
250+ ldpServerToken ,
251+ platform . toString ( ) ,
252+ serverPorts
253+ ) ;
254+
255+ const result : LocalDevServerResult = {
256+ url : ldpServerUrl ,
257+ serverId : ldpServerId ,
258+ token : ldpServerToken ,
259+ ports : serverPorts ,
260+ process : serverProcess ,
261+ } ;
262+
263+ // Store active server for cleanup
264+ this . activeServers . set ( ldpServerId , result ) ;
265+
266+ logger . info ( `LWC Dev Server started successfully at ${ ldpServerUrl } ` , {
267+ serverId : ldpServerId ,
268+ ports : serverPorts ,
269+ url : ldpServerUrl ,
270+ } ) ;
271+
272+ return result ;
273+ } catch ( error ) {
274+ logger . error ( 'Failed to start Local Dev Server' , {
275+ error : ( error as Error ) . message ,
276+ targetOrg : config . targetOrg ,
277+ } ) ;
278+ throw error ;
279+ }
280+ }
281+
282+ /**
283+ * Stop a specific server
284+ *
285+ * @param serverId Server ID to stop
286+ */
287+ public stopServer ( serverId : string ) : void {
288+ const server = this . activeServers . get ( serverId ) ;
289+ if ( server ?. process ) {
290+ server . process . stopServer ( ) ;
291+ this . activeServers . delete ( serverId ) ;
292+ }
293+ }
294+
295+ /**
296+ * Stop all active servers
297+ */
298+ public stopAllServers ( ) : void {
299+ const serverIds = Array . from ( this . activeServers . keys ( ) ) ;
300+ serverIds . forEach ( ( serverId ) => this . stopServer ( serverId ) ) ;
301+ }
302+ }
303+
304+ // Export the new programmatic API
305+ export const localDevServerManager = LocalDevServerManager . getInstance ( ) ;
0 commit comments