Skip to content

internal: (studio) set up hot reloading for the studio bundle #31796

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
May 30, 2025
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions guides/studio-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Regardless of running against local or deployed studio:

Note: When using the `CYPRESS_LOCAL_STUDIO_PATH` environment variable or when running the Cypress app via the locally cloned repository, we bypass our error reporting and instead log errors to the browser or node console.

Note: When using the `CYPRESS_LOCAL_STUDIO_PATH` the cloud studio code will be watched for changes so that you do not have to stop the app to incoprorate any new changes.

## Types

The studio bundle provides the types for the `app` and `server` interfaces that are used within the Cypress code. To incorporate the types into the code base, run:
Expand Down
23 changes: 21 additions & 2 deletions packages/app/src/studio/studio-app-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,18 @@ export type RunnerStatusShape = (props: RunnerStatusProps) => {

export interface StudioAIStreamProps {
canAccessStudioAI: boolean
AIOutputRef: { current: HTMLTextAreaElement | null }
runnerStatus: RunnerStatus
testCode?: string
isCreatingNewTest: boolean
}

export type StudioAIStreamShape = (props: StudioAIStreamProps) => void
export interface StudioAIStream {
recommendation: string
isStreaming: boolean
generationId: string | null
}

export type StudioAIStreamShape = (props: StudioAIStreamProps) => StudioAIStream

export interface TestContentRetrieverProps {
Cypress: CypressInternal
Expand All @@ -66,3 +71,17 @@ export type TestContentRetrieverShape = (props: TestContentRetrieverProps) => {
testBlock: TestBlock | null
isCreatingNewTest: boolean
}

export interface Command {
selector?: string
name: string
message?: string | string[]
isAssertion?: boolean
}

export interface SaveDetails {
absoluteFile: string
runnableTitle: string
contents: string
testName?: string
}

This file was deleted.

62 changes: 62 additions & 0 deletions packages/server/lib/cloud/api/studio/get_studio_bundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { asyncRetry, linearDelay } from '../../../util/async_retry'
import { isRetryableError } from '../../network/is_retryable_error'
import fetch from 'cross-fetch'
import os from 'os'
import { agent } from '@packages/network'
import { PUBLIC_KEY_VERSION } from '../../constants'
import { createWriteStream } from 'fs'
import { verifySignatureFromFile } from '../../encryption'

const pkg = require('@packages/root')
const _delay = linearDelay(500)

export const getStudioBundle = async ({ studioUrl, projectId, bundlePath }: { studioUrl: string, projectId?: string, bundlePath: string }) => {
let responseSignature: string | null = null

await (asyncRetry(async () => {
const response = await fetch(studioUrl, {
// @ts-expect-error - this is supported
agent,
method: 'GET',
headers: {
'x-route-version': '1',
'x-cypress-signature': PUBLIC_KEY_VERSION,
'x-os-name': os.platform(),
'x-cypress-version': pkg.version,
},
encrypt: 'signed',
})

if (!response.ok) {
throw new Error(`Failed to download studio bundle: ${response.statusText}`)
}

responseSignature = response.headers.get('x-cypress-signature')

await new Promise<void>((resolve, reject) => {
const writeStream = createWriteStream(bundlePath)

writeStream.on('error', reject)
writeStream.on('finish', () => {
resolve()
})

// @ts-expect-error - this is supported
response.body?.pipe(writeStream)
})
}, {
maxAttempts: 3,
retryDelay: _delay,
shouldRetry: isRetryableError,
}))()

if (!responseSignature) {
throw new Error('Unable to get studio signature')
}

const verified = await verifySignatureFromFile(bundlePath, responseSignature)

if (!verified) {
throw new Error('Unable to verify studio signature')
}
}
4 changes: 2 additions & 2 deletions packages/server/lib/cloud/api/studio/post_studio_session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { agent } from '@packages/network'
const pkg = require('@packages/root')
const routes = require('../../routes') as typeof import('../../routes')

interface GetStudioSessionOptions {
interface PostStudioSessionOptions {
projectId?: string
}

const _delay = linearDelay(500)

export const postStudioSession = async ({ projectId }: GetStudioSessionOptions) => {
export const postStudioSession = async ({ projectId }: PostStudioSessionOptions) => {
return await (asyncRetry(async () => {
const response = await fetch(routes.apiRoutes.studioSession(), {
// @ts-expect-error - this is supported
Expand Down
12 changes: 12 additions & 0 deletions packages/server/lib/cloud/get_cloud_metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { CloudDataSource } from '@packages/data-context/src/sources'

export const getCloudMetadata = async (cloudDataSource: CloudDataSource) => {
const cloudEnv = (process.env.CYPRESS_CONFIG_ENV || process.env.CYPRESS_INTERNAL_ENV || 'production') as 'development' | 'staging' | 'production'
const cloudUrl = cloudDataSource.getCloudUrl(cloudEnv)
const cloudHeaders = await cloudDataSource.additionalHeaders()

return {
cloudUrl,
cloudHeaders,
}
}
Loading
Loading