@@ -10,10 +10,14 @@ import path from 'path'
1010import os from 'os'
1111import { readFile } from 'fs-extra'
1212import { ensureCyPromptBundle } from './ensure_cy_prompt_bundle'
13+ import chokidar from 'chokidar'
14+ import { getCloudMetadata } from '../get_cloud_metadata'
1315
1416const debug = Debug ( 'cypress:server:cy-prompt-lifecycle-manager' )
1517
1618export class CyPromptLifecycleManager {
19+ private static hashLoadingMap : Map < string , Promise < void > > = new Map ( )
20+ private static watcher : chokidar . FSWatcher | null = null
1721 private cyPromptManagerPromise ?: Promise < CyPromptManager | null >
1822 private cyPromptManager ?: CyPromptManager
1923 private listeners : ( ( cyPromptManager : CyPromptManager ) => void ) [ ] = [ ]
@@ -72,6 +76,11 @@ export class CyPromptLifecycleManager {
7276 } )
7377
7478 this . cyPromptManagerPromise = cyPromptManagerPromise
79+
80+ this . setupWatcher ( {
81+ projectId,
82+ cloudDataSource,
83+ } )
7584 }
7685
7786 async getCyPrompt ( ) {
@@ -91,29 +100,42 @@ export class CyPromptLifecycleManager {
91100 projectId ?: string
92101 cloudDataSource : CloudDataSource
93102 } ) : Promise < CyPromptManager > {
103+ let cyPromptHash : string
104+ let cyPromptPath : string
105+
94106 const cyPromptSession = await postCyPromptSession ( {
95107 projectId,
96108 } )
97109
98- // The cy prompt hash is the last part of the cy prompt URL, after the last slash and before the extension
99- const cyPromptHash = cyPromptSession . cyPromptUrl . split ( '/' ) . pop ( ) ?. split ( '.' ) [ 0 ]
100- const cyPromptPath = path . join ( os . tmpdir ( ) , 'cypress' , 'cy-prompt' , cyPromptHash )
101- const bundlePath = path . join ( cyPromptPath , 'bundle.tar' )
102- const serverFilePath = path . join ( cyPromptPath , 'server' , 'index.js' )
110+ if ( ! process . env . CYPRESS_LOCAL_CY_PROMPT_PATH ) {
111+ // The cy prompt hash is the last part of the cy prompt URL, after the last slash and before the extension
112+ cyPromptHash = cyPromptSession . cyPromptUrl . split ( '/' ) . pop ( ) ?. split ( '.' ) [ 0 ]
113+ cyPromptPath = path . join ( os . tmpdir ( ) , 'cypress' , 'cy-prompt' , cyPromptHash )
103114
104- await ensureCyPromptBundle ( {
105- cyPromptUrl : cyPromptSession . cyPromptUrl ,
106- projectId,
107- cyPromptPath,
108- bundlePath,
109- } )
115+ let hashLoadingPromise = CyPromptLifecycleManager . hashLoadingMap . get ( cyPromptHash )
116+
117+ if ( ! hashLoadingPromise ) {
118+ hashLoadingPromise = ensureCyPromptBundle ( {
119+ cyPromptUrl : cyPromptSession . cyPromptUrl ,
120+ projectId,
121+ cyPromptPath,
122+ } )
123+
124+ CyPromptLifecycleManager . hashLoadingMap . set ( cyPromptHash , hashLoadingPromise )
125+ }
126+
127+ await hashLoadingPromise
128+ } else {
129+ cyPromptPath = process . env . CYPRESS_LOCAL_CY_PROMPT_PATH
130+ cyPromptHash = 'local'
131+ }
132+
133+ const serverFilePath = path . join ( cyPromptPath , 'server' , 'index.js' )
110134
111135 const script = await readFile ( serverFilePath , 'utf8' )
112136 const cyPromptManager = new CyPromptManager ( )
113137
114- const cloudEnv = ( process . env . CYPRESS_CONFIG_ENV || process . env . CYPRESS_INTERNAL_ENV || 'production' ) as 'development' | 'staging' | 'production'
115- const cloudUrl = cloudDataSource . getCloudUrl ( cloudEnv )
116- const cloudHeaders = await cloudDataSource . additionalHeaders ( )
138+ const { cloudUrl, cloudHeaders } = await getCloudMetadata ( cloudDataSource )
117139
118140 await cyPromptManager . setup ( {
119141 script,
@@ -148,7 +170,43 @@ export class CyPromptLifecycleManager {
148170 listener ( cyPromptManager )
149171 } )
150172
151- this . listeners = [ ]
173+ if ( ! process . env . CYPRESS_LOCAL_CY_PROMPT_PATH ) {
174+ this . listeners = [ ]
175+ }
176+ }
177+
178+ private setupWatcher ( {
179+ projectId,
180+ cloudDataSource,
181+ } : {
182+ projectId ?: string
183+ cloudDataSource : CloudDataSource
184+ } ) {
185+ // Don't setup a watcher if the cy prompt bundle is NOT local
186+ if ( ! process . env . CYPRESS_LOCAL_CY_PROMPT_PATH ) {
187+ return
188+ }
189+
190+ // Close the watcher if a previous watcher exists
191+ if ( CyPromptLifecycleManager . watcher ) {
192+ CyPromptLifecycleManager . watcher . removeAllListeners ( )
193+ CyPromptLifecycleManager . watcher . close ( ) . catch ( ( ) => { } )
194+ }
195+
196+ // Watch for changes to the cy prompt bundle
197+ CyPromptLifecycleManager . watcher = chokidar . watch ( path . join ( process . env . CYPRESS_LOCAL_CY_PROMPT_PATH , 'server' , 'index.js' ) , {
198+ awaitWriteFinish : true ,
199+ } ) . on ( 'change' , async ( ) => {
200+ this . cyPromptManager = undefined
201+ this . cyPromptManagerPromise = this . createCyPromptManager ( {
202+ projectId,
203+ cloudDataSource,
204+ } ) . catch ( ( error ) => {
205+ debug ( 'Error during reload of cy prompt manager: %o' , error )
206+
207+ return null
208+ } )
209+ } )
152210 }
153211
154212 /**
@@ -160,6 +218,12 @@ export class CyPromptLifecycleManager {
160218 if ( this . cyPromptManager ) {
161219 debug ( 'cy prompt ready - calling listener immediately' )
162220 listener ( this . cyPromptManager )
221+
222+ // If the cy prompt bundle is local, we need to register the listener
223+ // so that we can reload the cy prompt when the bundle changes
224+ if ( process . env . CYPRESS_LOCAL_CY_PROMPT_PATH ) {
225+ this . listeners . push ( listener )
226+ }
163227 } else {
164228 debug ( 'cy prompt not ready - registering cy prompt ready listener' )
165229 this . listeners . push ( listener )
0 commit comments