@@ -3,6 +3,7 @@ import { parseEnvelope } from '@sentry/core';
33import type { ChildProcess } from 'child_process' ;
44import { spawn } from 'child_process' ;
55import * as fs from 'fs' ;
6+ import * as http from 'http' ;
67import * as path from 'path' ;
78import * as readline from 'readline' ;
89import { fileURLToPath } from 'url' ;
@@ -49,10 +50,14 @@ const SPOTLIGHT_BIN = path.join(REPO_ROOT, 'node_modules', '.bin', 'spotlight');
4950interface SpotlightOptions {
5051 /** Port for the Spotlight sidecar. Use 0 for dynamic port assignment. */
5152 port ?: number ;
53+ /** Port for the app to run on. Defaults to 3030. */
54+ appPort ?: number ;
5255 /** Working directory for the child process (where package.json is located) */
5356 cwd ?: string ;
5457 /** Whether to enable debug output */
5558 debug ?: boolean ;
59+ /** Additional environment variables to pass to the process */
60+ env ?: Record < string , string > ;
5661}
5762
5863interface SpotlightInstance {
@@ -87,7 +92,7 @@ const eventListeners: Set<(envelope: Envelope) => void> = new Set();
8792 * - Streams events in JSON format (with -f json flag)
8893 */
8994export async function startSpotlight ( options : SpotlightOptions = { } ) : Promise < SpotlightInstance > {
90- const { port = 0 , cwd = process . cwd ( ) , debug = false } = options ;
95+ const { port = 0 , appPort = 3030 , cwd = process . cwd ( ) , debug = false , env = { } } = options ;
9196
9297 return new Promise ( ( resolve , reject ) => {
9398 // Run Spotlight directly from repo root's node_modules
@@ -104,6 +109,11 @@ export async function startSpotlight(options: SpotlightOptions = {}): Promise<Sp
104109 const spotlightProcess = spawn ( SPOTLIGHT_BIN , args , {
105110 cwd,
106111 stdio : [ 'ignore' , 'pipe' , 'pipe' ] ,
112+ env : {
113+ ...process . env ,
114+ ...env ,
115+ PORT : String ( appPort ) ,
116+ } ,
107117 } ) ;
108118
109119 let resolvedPort : number | null = null ;
@@ -121,6 +131,30 @@ export async function startSpotlight(options: SpotlightOptions = {}): Promise<Sp
121131 crlfDelay : Infinity ,
122132 } ) ;
123133
134+ // Helper to poll the app port until it's ready
135+ const waitForAppReady = async ( ) : Promise < void > => {
136+ const maxAttempts = 60 ; // 30 seconds
137+ for ( let i = 0 ; i < maxAttempts ; i ++ ) {
138+ try {
139+ await new Promise < void > ( ( resolveCheck , rejectCheck ) => {
140+ const req = http . get ( `http://localhost:${ appPort } /` , res => {
141+ res . resume ( ) ;
142+ resolveCheck ( ) ;
143+ } ) ;
144+ req . on ( 'error' , rejectCheck ) ;
145+ req . setTimeout ( 500 , ( ) => {
146+ req . destroy ( ) ;
147+ rejectCheck ( new Error ( 'timeout' ) ) ;
148+ } ) ;
149+ } ) ;
150+ return ; // App is ready
151+ } catch {
152+ await new Promise ( r => setTimeout ( r , 500 ) ) ;
153+ }
154+ }
155+ throw new Error ( `App did not start on port ${ appPort } within 30 seconds` ) ;
156+ } ;
157+
124158 stderrReader . on ( 'line' , ( line : string ) => {
125159 if ( debug ) {
126160 // eslint-disable-next-line no-console
@@ -136,21 +170,19 @@ export async function startSpotlight(options: SpotlightOptions = {}): Promise<Sp
136170
137171 if ( portMatch ?. [ 1 ] && ! resolvedPort ) {
138172 resolvedPort = parseInt ( portMatch [ 1 ] , 10 ) ;
139- // Resolve immediately when we have the port from a "listening" message
140- if ( ! resolved && line . includes ( 'listening' ) ) {
141- resolved = true ;
142- const instance = createSpotlightInstance ( spotlightProcess , resolvedPort , debug ) ;
143- currentSpotlightInstance = instance ;
144- resolve ( instance ) ;
145- }
146173 }
147174
148- // Fallback: check for other ready messages if we have a port
149- if ( ! resolved && resolvedPort && ( line . includes ( 'running' ) || line . includes ( 'started' ) ) ) {
175+ // When Spotlight says it's listening, start waiting for the app
176+ if ( ! resolved && resolvedPort && line . includes ( 'listening' ) ) {
150177 resolved = true ;
151- const instance = createSpotlightInstance ( spotlightProcess , resolvedPort , debug ) ;
152- currentSpotlightInstance = instance ;
153- resolve ( instance ) ;
178+ // Wait for app to be ready before resolving
179+ waitForAppReady ( )
180+ . then ( ( ) => {
181+ const instance = createSpotlightInstance ( spotlightProcess , resolvedPort , debug ) ;
182+ currentSpotlightInstance = instance ;
183+ resolve ( instance ) ;
184+ } )
185+ . catch ( reject ) ;
154186 }
155187 } ) ;
156188
0 commit comments