Skip to content

Commit e18d31a

Browse files
chore: add watcher for cy-prompt development (#31810)
* chore: add watcher for cy-prompt development * test caching * fix types
1 parent 22737d2 commit e18d31a

File tree

4 files changed

+302
-92
lines changed

4 files changed

+302
-92
lines changed

packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,14 @@ import path from 'path'
1010
import os from 'os'
1111
import { readFile } from 'fs-extra'
1212
import { ensureCyPromptBundle } from './ensure_cy_prompt_bundle'
13+
import chokidar from 'chokidar'
14+
import { getCloudMetadata } from '../get_cloud_metadata'
1315

1416
const debug = Debug('cypress:server:cy-prompt-lifecycle-manager')
1517

1618
export 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)
Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { copy, remove, ensureDir } from 'fs-extra'
1+
import { remove, ensureDir } from 'fs-extra'
22

33
import tar from 'tar'
44
import { getCyPromptBundle } from '../api/cy-prompt/get_cy_prompt_bundle'
@@ -8,30 +8,23 @@ interface EnsureCyPromptBundleOptions {
88
cyPromptPath: string
99
cyPromptUrl: string
1010
projectId?: string
11-
bundlePath: string
1211
}
1312

14-
export const ensureCyPromptBundle = async ({ cyPromptPath, cyPromptUrl, projectId, bundlePath }: EnsureCyPromptBundleOptions) => {
13+
export const ensureCyPromptBundle = async ({ cyPromptPath, cyPromptUrl, projectId }: EnsureCyPromptBundleOptions) => {
14+
const bundlePath = path.join(cyPromptPath, 'bundle.tar')
15+
1516
// First remove cyPromptPath to ensure we have a clean slate
1617
await remove(cyPromptPath)
1718
await ensureDir(cyPromptPath)
1819

19-
if (!process.env.CYPRESS_LOCAL_CY_PROMPT_PATH) {
20-
await getCyPromptBundle({
21-
cyPromptUrl,
22-
projectId,
23-
bundlePath,
24-
})
25-
26-
await tar.extract({
27-
file: bundlePath,
28-
cwd: cyPromptPath,
29-
})
30-
} else {
31-
const driverPath = path.join(process.env.CYPRESS_LOCAL_CY_PROMPT_PATH, 'driver')
32-
const serverPath = path.join(process.env.CYPRESS_LOCAL_CY_PROMPT_PATH, 'server')
20+
await getCyPromptBundle({
21+
cyPromptUrl,
22+
projectId,
23+
bundlePath,
24+
})
3325

34-
await copy(driverPath, path.join(cyPromptPath, 'driver'))
35-
await copy(serverPath, path.join(cyPromptPath, 'server'))
36-
}
26+
await tar.extract({
27+
file: bundlePath,
28+
cwd: cyPromptPath,
29+
})
3730
}

0 commit comments

Comments
 (0)