chore: add logic to dynamically load studio functionality (#31033)

* chore: add logic to dynamically load new studio functionality

* fix types

* fix tests

* fix

* fix tests

* fix tests

* additional changes to lock things down

* clean up code

* Update guides/studio-development.md

Co-authored-by: Jennifer Shehane <jennifer@cypress.io>

* Update protocol-development.md

* additional headers

* PR comments

* Update packages/server/lib/cloud/api/get_app_studio.ts

Co-authored-by: Matt Schile <mschile@cypress.io>

* Update packages/app/vite.config.mjs

* update studio/protocol development guides

---------

Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
Co-authored-by: Matt Schile <mschile@cypress.io>
This commit is contained in:
Ryan Manuel
2025-02-14 20:54:15 +00:00
committed by GitHub
parent 354b4f2126
commit 2dce6d5831
44 changed files with 1197 additions and 196 deletions

View File

@@ -39,6 +39,7 @@ mainBuildFilters: &mainBuildFilters
- chore/update_wdio_deps
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- 'update-v8-snapshot-cache-on-develop'
- 'ryanm/chore/add_internal_studio'
# usually we don't build Mac app - it takes a long time
# but sometimes we want to really confirm we are doing the right thing
@@ -49,7 +50,7 @@ macWorkflowFilters: &darwin-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'chore/browser_spike', << pipeline.git.branch >> ]
- equal: [ 'ryanm/chore/add_internal_studio', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
@@ -60,7 +61,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'chore/browser_spike', << pipeline.git.branch >> ]
- equal: [ 'ryanm/chore/add_internal_studio', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
@@ -83,7 +84,7 @@ windowsWorkflowFilters: &windows-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'fix/em_dash_priv_command', << pipeline.git.branch >> ]
- equal: [ 'ryanm/chore/add_internal_studio', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
@@ -159,7 +160,7 @@ commands:
name: Set environment variable to determine whether or not to persist artifacts
command: |
echo "Setting SHOULD_PERSIST_ARTIFACTS variable"
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* ]]; then
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "ryanm/chore/add_internal_studio" ]]; then
export SHOULD_PERSIST_ARTIFACTS=true
fi' >> "$BASH_ENV"
# You must run `setup_should_persist_artifacts` command and be using bash before running this command

View File

@@ -2,9 +2,9 @@
In production, the capture code used to capture and communicate test data will be retrieved from the Cloud. However, in order to develop the capture code locally, developers will:
* Clone the `cypress-services` repo
* Clone the `cypress-services` repo (this requires that you be a member of the Cypress organization)
* Run `yarn`
* Run `yarn watch` in `packages/app-capture-protocol`
* Run `yarn watch` in `app/capture-protocol`
* Clone the `cypress` repo
* Run `yarn`
* Execute `CYPRESS_LOCAL_PROTOCOL_PATH=path/to/cypress-services/packages/app-capture-protocol/dist/index.js CYPRESS_INTERNAL_ENV=staging yarn cypress:run --record --key <record_key> --project <path/to/project>` on a project in record mode
* Execute `CYPRESS_LOCAL_PROTOCOL_PATH=path/to/cypress-services/app/capture-protocol/dist/index.js CYPRESS_INTERNAL_ENV=staging yarn cypress:run --record --key <record_key> --project <path/to/project>` on a project in record mode

View File

@@ -0,0 +1,11 @@
# Studio Development
In production, the code used to facilitate Studio functionality will be retrieved from the Cloud. However, in order to develop locally, developers will:
- Clone the `cypress-services` repo (this requires that you be a member of the Cypress organization)
- Run `yarn`
- Run `yarn watch` in `app/studio`
- Set `CYPRESS_LOCAL_STUDIO_PATH` to the path to the `cypress-services/app/studio/dist/development` directory
- Clone the `cypress` repo
- Run `yarn`
- Run `yarn cypress:open`

View File

@@ -24,7 +24,7 @@
"@types/node": "^20.16.0",
"chai-as-promised": "^7.1.1",
"chokidar": "^3.5.3",
"express": "4.19.2",
"express": "4.21.0",
"mocha": "^9.2.2",
"rimraf": "^5.0.10",
"semantic-release": "22.0.12",

View File

@@ -33,6 +33,7 @@
"@headlessui/vue": "1.4.0",
"@iconify-json/mdi": "1.1.63",
"@intlify/unplugin-vue-i18n": "4.0.0",
"@module-federation/vite": "^1.2.2",
"@packages/data-context": "0.0.0-development",
"@packages/frontend-shared": "0.0.0-development",
"@packages/graphql": "0.0.0-development",

View File

@@ -101,7 +101,7 @@
</template>
<script lang="ts" setup>
import { computed, onBeforeUnmount, onMounted } from 'vue'
import { computed, onBeforeUnmount, onMounted, watchEffect } from 'vue'
import { REPORTER_ID, RUNNER_ID } from './utils'
import InlineSpecList from '../specs/InlineSpecList.vue'
import { getAutIframeModel, getEventManager } from '.'
@@ -153,6 +153,14 @@ fragment SpecRunner_Preferences on Query {
}
`
gql`
fragment SpecRunner_Studio on Query {
studio {
status
}
}
`
gql`
fragment SpecRunner_Config on CurrentProject {
id
@@ -171,6 +179,7 @@ fragment SpecRunner on Query {
}
...ChooseExternalEditor
...SpecRunner_Preferences
...SpecRunner_Studio
}
`
@@ -215,6 +224,10 @@ const isSpecsListOpenPreferences = computed(() => {
return props.gql.localSettings.preferences.isSpecsListOpen ?? false
})
const studioStatus = computed(() => {
return props.gql.studio?.status
})
const hideCommandLog = runnerUiStore.hideCommandLog
// watch active spec, and re-run if it changes!
@@ -286,6 +299,19 @@ function openFile () {
},
})
}
watchEffect(() => {
if (studioStatus.value === 'INITIALIZED') {
import('app-studio').then(({ mountTestGenerationPanel }) => {
// eslint-disable-next-line no-console
console.log('Studio loaded', mountTestGenerationPanel)
}).catch((err) => {
// eslint-disable-next-line no-console
console.error('Error loading Studio', err)
})
}
})
onMounted(() => {
const eventManager = getEventManager()

7
packages/app/src/studio/index.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
declare module 'app-studio' {
export const mountTestGenerationPanel = (
reactInstance: any,
reactDOMInstance: any,
container: HTMLElement,
) => {}
}

View File

@@ -4,8 +4,9 @@ import Pages from 'vite-plugin-pages'
import Copy from 'rollup-plugin-copy'
import Legacy from '@vitejs/plugin-legacy'
import { resolve } from 'path'
import { federation } from '@module-federation/vite'
export default makeConfig({
const config = makeConfig({
optimizeDeps: {
include: [
'javascript-time-ago',
@@ -20,13 +21,13 @@ export default makeConfig({
'@popperjs/core',
'@opentelemetry/*',
],
esbuildOptions: {
target: "ES2022"
}
esbuildOptions: {
target: 'ES2022',
},
},
build: {
target: "ES2022"
}
target: 'ES2022',
},
}, {
plugins: [
Layouts(),
@@ -44,3 +45,24 @@ export default makeConfig({
}),
],
})
// With some trial and error, it appears that the module federation plugin needs to be added
// to the plugins array first so that the dynamic modules are available properly with respect
// to the other plugins.
config.plugins.unshift(
...federation({
name: 'host',
remotes: {
'app-studio': {
type: 'module',
name: 'app-studio',
entryGlobalName: 'app-studio',
entry: '/__cypress-studio/app-studio.js',
shareScope: 'default',
},
},
filename: 'assets/app-studio.js',
}),
)
export default config

View File

@@ -1,4 +1,4 @@
import { FoundBrowser, Editor, AllowedState, AllModeOptions, TestingType, BrowserStatus, PACKAGE_MANAGERS, AuthStateName, MIGRATION_STEPS, MigrationStep, BannerState } from '@packages/types'
import { FoundBrowser, Editor, AllowedState, AllModeOptions, TestingType, BrowserStatus, PACKAGE_MANAGERS, AuthStateName, MIGRATION_STEPS, MigrationStep, BannerState, StudioManagerShape } from '@packages/types'
import { WizardBundler, CT_FRAMEWORKS, resolveComponentFrameworkDefinition, ErroredFramework } from '@packages/scaffold-config'
import type { NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen'
// tslint:disable-next-line no-implicit-dependencies - electron dep needs to be defined
@@ -173,6 +173,7 @@ export interface CoreDataShape {
cloudProject: CloudDataShape
eventCollectorSource: EventCollectorSource | null
didBrowserPreviouslyHaveUnexpectedExit: boolean
studio: StudioManagerShape | null
}
/**
@@ -254,6 +255,7 @@ export function makeCoreData (modeOptions: Partial<AllModeOptions> = {}): CoreDa
},
eventCollectorSource: null,
didBrowserPreviouslyHaveUnexpectedExit: false,
studio: null,
}
async function machineId (): Promise<string | null> {

View File

@@ -61,7 +61,7 @@
"error-stack-parser": "2.0.6",
"errorhandler": "1.5.1",
"eventemitter2": "6.4.7",
"express": "4.19.2",
"express": "4.21.0",
"is-valid-domain": "0.0.20",
"is-valid-hostname": "1.0.1",
"jimp": "0.22.12",

View File

@@ -116,7 +116,6 @@ export const makeConfig = (config = {}, plugins = {}) => {
css: {
preprocessorOptions: {
scss: {
// @ts-expect-error
additionalData: `@use "file:///${path.resolve(__dirname, '../reporter/src/lib/variables.scss').replaceAll('\\', '/')}" as *;\n`,
},
},

View File

@@ -2071,6 +2071,9 @@ type Query {
"""The files that have just been scaffolded"""
scaffoldedFiles: [ScaffoldedFile!]
"""Studio data"""
studio: Studio
"""Previous versions of cypress and their release date"""
versions: VersionData
@@ -2370,6 +2373,18 @@ enum SpecType {
integration
}
"""The studio manager for the app"""
type Studio {
"""The current status of the studio"""
status: StudioStatusType
}
enum StudioStatusType {
INITIALIZED
IN_ERROR
NOT_INITIALIZED
}
type Subscription {
"""Triggered when the auth state changes"""
authChange: Query

View File

@@ -11,6 +11,7 @@ import { Wizard } from './gql-Wizard'
import { ErrorWrapper } from './gql-ErrorWrapper'
import { CachedUser } from './gql-CachedUser'
import { Cohort } from './gql-Cohorts'
import { Studio } from './gql-Studio'
export const Query = objectType({
name: 'Query',
@@ -101,6 +102,12 @@ export const Query = objectType({
resolve: (source, args, ctx) => ctx.coreData.authState,
})
t.field('studio', {
type: Studio,
description: 'Data pertaining to studio and the studio manager that is loaded from the cloud',
resolve: (source, args, ctx) => ctx.coreData.studio,
})
t.nonNull.field('localSettings', {
type: LocalSettings,
description: 'local settings on a device-by-device basis',

View File

@@ -0,0 +1,18 @@
import { STUDIO_STATUSES } from '@packages/types'
import { enumType, objectType } from 'nexus'
export const StudioStatusTypeEnum = enumType({
name: 'StudioStatusType',
members: STUDIO_STATUSES,
})
export const Studio = objectType({
name: 'Studio',
description: 'The studio manager for the app',
definition (t) {
t.field('status', {
type: StudioStatusTypeEnum,
description: 'The current status of the studio',
})
},
})

View File

@@ -28,6 +28,7 @@ export * from './gql-RunSpecError'
export * from './gql-RunSpecResponse'
export * from './gql-ScaffoldedFile'
export * from './gql-Spec'
export * from './gql-Studio'
export * from './gql-Subscription'
export * from './gql-TestingTypeInfo'
export * from './gql-Version'

View File

@@ -34,7 +34,7 @@
"@packages/socket": "0.0.0-development",
"@packages/ts": "0.0.0-development",
"@types/concat-stream": "1.6.0",
"express": "4.19.2",
"express": "4.21.0",
"mocha": "6.2.2",
"sinon": "7.3.1",
"sinon-chai": "3.3.0",

View File

@@ -45,7 +45,7 @@
"@types/express": "4.17.2",
"@types/supertest": "2.0.10",
"devtools-protocol": "0.0.1413303",
"express": "4.19.2",
"express": "4.21.0",
"supertest": "6.0.1",
"typescript": "~5.4.5"
},

View File

@@ -0,0 +1,135 @@
import path from 'path'
import os from 'os'
import { ensureDir, copy, readFile } from 'fs-extra'
import cloudApi from '.'
import { StudioManager } from '../studio'
import tar from 'tar'
import { verifySignatureFromFile } from '../encryption'
import crypto from 'crypto'
import fs from 'fs'
import fetch from 'cross-fetch'
import { agent } from '@packages/network'
import { asyncRetry, linearDelay } from '../../util/async_retry'
import { isRetryableError } from '../network/is_retryable_error'
const pkg = require('@packages/root')
const routes = require('../routes')
const _delay = linearDelay(500)
const studioPath = path.join(os.tmpdir(), 'cypress', 'studio')
const bundlePath = path.join(studioPath, 'bundle.tar')
const serverFilePath = path.join(studioPath, 'server', 'index.js')
const downloadAppStudioBundleToTempDirectory = async (projectId?: string): Promise<void> => {
let responseSignature: string | null = null
await (asyncRetry(async () => {
const response = await fetch(routes.apiRoutes.studio() as string, {
// @ts-expect-error - this is supported
agent,
method: 'GET',
headers: {
'x-route-version': '1',
'x-cypress-signature': cloudApi.publicKeyVersion,
...(projectId ? { 'x-cypress-project-slug': projectId } : {}),
'x-cypress-studio-mount-version': '1',
'x-os-name': os.platform(),
'x-cypress-version': pkg.version,
},
encrypt: 'signed',
})
responseSignature = response.headers.get('x-cypress-signature')
await new Promise<void>((resolve, reject) => {
const writeStream = fs.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')
}
}
const getTarHash = (): Promise<string> => {
let hash = ''
return new Promise<string>((resolve, reject) => {
fs.createReadStream(bundlePath)
.pipe(crypto.createHash('sha256'))
.setEncoding('base64url')
.on('data', (data) => {
hash += String(data)
})
.on('error', reject)
.on('close', () => {
resolve(hash)
})
})
}
export const getAppStudio = async (projectId?: string): Promise<StudioManager> => {
try {
let script: string
let studioHash: string | undefined
// First remove studioPath to ensure we have a clean slate
await fs.promises.rm(studioPath, { recursive: true, force: true })
await ensureDir(studioPath)
// Note: CYPRESS_LOCAL_STUDIO_PATH is stripped from the binary, effectively removing this code path
if (process.env.CYPRESS_LOCAL_STUDIO_PATH) {
const appPath = path.join(process.env.CYPRESS_LOCAL_STUDIO_PATH, 'app')
const serverPath = path.join(process.env.CYPRESS_LOCAL_STUDIO_PATH, 'server')
await copy(appPath, path.join(studioPath, 'app'))
await copy(serverPath, path.join(studioPath, 'server'))
} else {
await downloadAppStudioBundleToTempDirectory(projectId)
studioHash = await getTarHash()
await tar.extract({
file: bundlePath,
cwd: studioPath,
})
}
script = await readFile(serverFilePath, 'utf8')
const appStudio = new StudioManager()
appStudio.setup({ script, studioPath, studioHash })
return appStudio
} catch (error: unknown) {
let actualError: Error
if (!(error instanceof Error)) {
actualError = new Error(String(error))
} else {
actualError = error
}
return StudioManager.createInErrorManager(actualError)
}
}

View File

@@ -692,4 +692,6 @@ export default {
},
retryWithBackoff,
publicKeyVersion: PUBLIC_KEY_VERSION,
}

View File

@@ -4,6 +4,7 @@ import { generalDecrypt, GeneralJWE } from 'jose'
import base64Url from 'base64url'
import type { CypressRequestOptions } from './api'
import { deflateRaw as deflateRawCb } from 'zlib'
import fs from 'fs'
const deflateRaw = promisify(deflateRawCb)
@@ -41,7 +42,26 @@ export function verifySignature (body: string, signature: string, publicKey?: cr
verify.update(body)
return verify.verify(publicKey || getPublicKey(), Buffer.from(signature, 'base64'))
return verify.verify(publicKey || getPublicKey(), signature, 'base64')
}
export function verifySignatureFromFile (file: string, signature: string, publicKey?: crypto.KeyObject): Promise<boolean> {
const verify = crypto.createVerify('SHA256')
const stream = fs.createReadStream(file)
stream.on('data', (chunk: crypto.BinaryLike) => {
verify.update(chunk)
})
return new Promise<boolean>((resolve, reject) => {
stream.on('end', () => {
verify.end()
resolve(verify.verify(publicKey || getPublicKey(), signature, 'base64'))
})
stream.on('error', reject)
})
}
// Implements the https://www.rfc-editor.org/rfc/rfc7516 spec

View File

@@ -4,13 +4,13 @@ import fetch from 'cross-fetch'
import crypto from 'crypto'
import Debug from 'debug'
import fs from 'fs-extra'
import Module from 'module'
import os from 'os'
import path from 'path'
import { agent } from '@packages/network'
import pkg from '@packages/root'
import env from '../util/env'
import { putProtocolArtifact } from './api/put_protocol_artifact'
import { requireScript } from './require_script'
import type { Readable } from 'stream'
import type { ProtocolManagerShape, AppCaptureProtocolInterface, CDPClient, ProtocolError, CaptureArtifact, ProtocolErrorReport, ProtocolCaptureMethod, ProtocolManagerOptions, ResponseStreamOptions, ResponseEndedWithEmptyBodyOptions, ResponseStreamTimedOutOptions, AfterSpecDurations, SpecWithRelativeRoot } from '@packages/types'
@@ -32,24 +32,6 @@ const dbSizeLimit = () => {
200 : DB_SIZE_LIMIT
}
/**
* requireScript, does just that, requires the passed in script as if it was a module.
* @param script - string
* @returns exports
*/
const requireScript = (script: string) => {
const mod = new Module('id', module)
mod.filename = ''
// _compile is a private method
// @ts-expect-error
mod._compile(script, mod.filename)
module.children.splice(module.children.indexOf(mod), 1)
return mod.exports
}
export class ProtocolManager implements ProtocolManagerShape {
private _runId?: string
private _instanceId?: string

View File

@@ -0,0 +1,19 @@
import Module from 'module'
/**
* requireScript, does just that, requires the passed in script as if it was a module.
* @param script - string
* @returns exports
*/
export const requireScript = (script: string) => {
const mod = new Module('id', module)
mod.filename = ''
// _compile is a private method
// @ts-expect-error
mod._compile(script, mod.filename)
module.children.splice(module.children.indexOf(mod), 1)
return mod.exports
}

View File

@@ -15,6 +15,8 @@ const CLOUD_ENDPOINTS = {
instanceStdout: 'instances/:id/stdout',
instanceArtifacts: 'instances/:id/artifacts',
captureProtocolErrors: 'capture-protocol/errors',
studio: 'studio/bundle/current.tgz',
studioErrors: 'studio/errors',
exceptions: 'exceptions',
telemetry: 'telemetry',
} as const

View File

@@ -0,0 +1,136 @@
import type { AppStudioShape, StudioErrorReport, StudioManagerShape, StudioStatus } from '@packages/types'
import type { Router } from 'express'
import fetch from 'cross-fetch'
import pkg from '@packages/root'
import os from 'os'
import { agent } from '@packages/network'
import Debug from 'debug'
import { requireScript } from './require_script'
const debug = Debug('cypress:server:studio')
const routes = require('./routes')
export class StudioManager implements StudioManagerShape {
status: StudioStatus = 'NOT_INITIALIZED'
private _appStudio: AppStudioShape | undefined
private _studioHash: string | undefined
static createInErrorManager (error: Error): StudioManager {
const manager = new StudioManager()
manager.status = 'IN_ERROR'
manager.reportError(error).catch(() => { })
return manager
}
setup ({ script, studioPath, studioHash }: { script: string, studioPath: string, studioHash?: string }): void {
const { AppStudio } = requireScript(script)
this._appStudio = new AppStudio({ studioPath })
this._studioHash = studioHash
this.status = 'INITIALIZED'
}
initializeRoutes (router: Router): void {
if (this._appStudio) {
this.invokeSync('initializeRoutes', { isEssential: true }, router)
}
}
private async reportError (error: Error): Promise<void> {
try {
const payload: StudioErrorReport = {
studioHash: this._studioHash,
errors: [{
name: error.name ?? `Unknown name`,
stack: error.stack ?? `Unknown stack`,
message: error.message ?? `Unknown message`,
}],
}
const body = JSON.stringify(payload)
await fetch(routes.apiRoutes.studioErrors() as string, {
// @ts-expect-error - this is supported
agent,
method: 'POST',
body,
headers: {
'Content-Type': 'application/json',
'x-cypress-version': pkg.version,
'x-os-name': os.platform(),
'x-arch': os.arch(),
},
})
} catch (e) {
debug(`Error calling StudioManager.reportError: %o, original error %o`, e, error)
}
}
/**
* Abstracts invoking a synchronous method on the AppStudio instance, so we can handle
* errors in a uniform way
*/
private invokeSync<K extends AppStudioSyncMethods> (method: K, { isEssential }: { isEssential: boolean }, ...args: Parameters<AppStudioShape[K]>): any | void {
if (!this._appStudio) {
return
}
try {
return this._appStudio[method].apply(this._appStudio, args)
} catch (error: unknown) {
let actualError: Error
if (!(error instanceof Error)) {
actualError = new Error(String(error))
} else {
actualError = error
}
this.status = 'IN_ERROR'
// Call and forget this, we don't want to block the main thread
this.reportError(actualError).catch(() => { })
}
}
/**
* Abstracts invoking a synchronous method on the AppStudio instance, so we can handle
* errors in a uniform way
*/
private async invokeAsync <K extends AppStudioAsyncMethods> (method: K, { isEssential }: { isEssential: boolean }, ...args: Parameters<AppStudioShape[K]>): Promise<ReturnType<AppStudioShape[K]> | undefined> {
if (!this._appStudio) {
return undefined
}
try {
// @ts-expect-error - TS not associating the method & args properly, even though we know it's correct
return await this._appStudio[method].apply(this._appStudio, args)
} catch (error: unknown) {
let actualError: Error
if (!(error instanceof Error)) {
actualError = new Error(String(error))
} else {
actualError = error
}
this.status = 'IN_ERROR'
// Call and forget this, we don't want to block the main thread
this.reportError(actualError).catch(() => { })
// TODO: Figure out errors
return undefined
}
}
}
// Helper types for invokeSync / invokeAsync
type AppStudioSyncMethods = {
[K in keyof AppStudioShape]: ReturnType<AppStudioShape[K]> extends Promise<any> ? never : K
}[keyof AppStudioShape]
type AppStudioAsyncMethods = {
[K in keyof AppStudioShape]: ReturnType<AppStudioShape[K]> extends Promise<any> ? K : never
}[keyof AppStudioShape]

View File

@@ -18,13 +18,14 @@ import { SocketE2E } from './socket-e2e'
import { ensureProp } from './util/class-helpers'
import system from './util/system'
import type { BannersState, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ReceivedCypressOptions, ResolvedConfigurationOptions, TestingType, VideoRecording } from '@packages/types'
import type { BannersState, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ReceivedCypressOptions, ResolvedConfigurationOptions, StudioManagerShape, TestingType, VideoRecording } from '@packages/types'
import { DataContext, getCtx } from '@packages/data-context'
import { createHmac } from 'crypto'
import type ProtocolManager from './cloud/protocol'
import { ServerBase } from './server-base'
import type Protocol from 'devtools-protocol'
import type { ServiceWorkerClientEvent } from '@packages/proxy/lib/http/util/service-worker-manager'
import { getAppStudio } from './cloud/api/get_app_studio'
export interface Cfg extends ReceivedCypressOptions {
projectId?: string
@@ -153,6 +154,15 @@ export class ProjectBase extends EE {
this._server = new ServerBase(cfg)
let appStudio: StudioManagerShape | null
if (process.env.CYPRESS_ENABLE_CLOUD_STUDIO || process.env.CYPRESS_LOCAL_STUDIO_PATH) {
appStudio = await getAppStudio(cfg.projectId)
this.ctx.update((data) => {
data.studio = appStudio
})
}
const [port, warning] = await this._server.open(cfg, {
getCurrentBrowser: () => this.browser,
getSpec: () => this.spec,

View File

@@ -104,6 +104,18 @@ export const createCommonRoutes = ({
next()
})
// We need to handle the case where the studio is not defined or loaded properly.
// Module federation still tries to load the dynamic asset, but since we do not
// have anything to load, we return a blank file.
if (!getCtx().coreData.studio || getCtx().coreData.studio?.status === 'IN_ERROR') {
router.get('/__cypress-studio/app-studio.js', (req, res) => {
res.setHeader('Content-Type', 'application/javascript')
res.status(200).send('')
})
} else {
getCtx().coreData.studio?.initializeRoutes(router)
}
router.get(`/${config.namespace}/tests`, (req, res, next) => {
// slice out the cache buster
const test = CacheBuster.strip(req.query.p)

View File

@@ -123,6 +123,7 @@
"strip-ansi": "6.0.1",
"syntax-error": "1.4.0",
"systeminformation": "5.21.7",
"tar": "^6.1.0",
"term-size": "2.1.0",
"through": "2.3.8",
"tough-cookie": "4.1.3",
@@ -171,6 +172,7 @@
"@types/http-proxy": "1.17.4",
"@types/node": "20.16.0",
"@types/request-promise": "^4.1.48",
"@types/tar": "^6.1.0",
"babel-loader": "9.1.3",
"chai": "1.10.0",
"chai-as-promised": "7.1.1",

View File

@@ -0,0 +1,2 @@
// eslint-disable-next-line no-console
console.log('studio script')

View File

@@ -0,0 +1,8 @@
import type { AppStudioShape } from '@packages/types'
import type { Router } from 'express'
export class AppStudio implements AppStudioShape {
initializeRoutes (router: Router): void {
}
}

View File

@@ -0,0 +1,307 @@
import { Readable, Writable } from 'stream'
import { proxyquire, sinon } from '../../../spec_helper'
import { HttpError } from '../../../../lib/cloud/network/http_error'
describe('getAppStudio', () => {
let getAppStudio: typeof import('@packages/server/lib/cloud/api/get_app_studio').getAppStudio
let rmStub: sinon.SinonStub = sinon.stub()
let ensureStub: sinon.SinonStub = sinon.stub()
let copyStub: sinon.SinonStub = sinon.stub()
let readFileStub: sinon.SinonStub = sinon.stub()
let crossFetchStub: sinon.SinonStub = sinon.stub()
let createReadStreamStub: sinon.SinonStub = sinon.stub()
let createWriteStreamStub: sinon.SinonStub = sinon.stub()
let verifySignatureFromFileStub: sinon.SinonStub = sinon.stub()
let extractStub: sinon.SinonStub = sinon.stub()
let createInErrorManagerStub: sinon.SinonStub = sinon.stub()
let tmpdir: string = '/tmp'
let studioManagerSetupStub: sinon.SinonStub = sinon.stub()
beforeEach(() => {
rmStub = sinon.stub()
ensureStub = sinon.stub()
copyStub = sinon.stub()
readFileStub = sinon.stub()
crossFetchStub = sinon.stub()
createReadStreamStub = sinon.stub()
createWriteStreamStub = sinon.stub()
verifySignatureFromFileStub = sinon.stub()
extractStub = sinon.stub()
createInErrorManagerStub = sinon.stub()
studioManagerSetupStub = sinon.stub()
getAppStudio = (proxyquire('../lib/cloud/api/get_app_studio', {
fs: {
promises: {
rm: rmStub.resolves(),
},
createReadStream: createReadStreamStub,
createWriteStream: createWriteStreamStub,
},
os: {
tmpdir: () => tmpdir,
platform: () => 'linux',
},
'fs-extra': {
ensureDir: ensureStub.resolves(),
copy: copyStub.resolves(),
readFile: readFileStub.resolves('console.log("studio script")'),
},
tar: {
extract: extractStub.resolves(),
},
'../encryption': {
verifySignatureFromFile: verifySignatureFromFileStub,
},
'../studio': {
StudioManager: class StudioManager {
static createInErrorManager = createInErrorManagerStub
setup = (...options) => studioManagerSetupStub(...options)
},
},
'cross-fetch': crossFetchStub,
'@packages/root': {
version: '1.2.3',
},
}) as typeof import('@packages/server/lib/cloud/api/get_app_studio')).getAppStudio
})
afterEach(() => {
sinon.restore()
})
describe('CYPRESS_LOCAL_STUDIO_PATH is set', () => {
beforeEach(() => {
process.env.CYPRESS_LOCAL_STUDIO_PATH = '/path/to/studio'
})
it('gets the studio bundle from the path specified in the environment variable', async () => {
await getAppStudio()
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
expect(copyStub).to.be.calledWith('/path/to/studio/app', '/tmp/cypress/studio/app')
expect(copyStub).to.be.calledWith('/path/to/studio/server', '/tmp/cypress/studio/server')
expect(readFileStub).to.be.calledWith('/tmp/cypress/studio/server/index.js', 'utf8')
expect(studioManagerSetupStub).to.be.calledWithMatch({
script: 'console.log("studio script")',
studioPath: '/tmp/cypress/studio',
studioHash: undefined,
})
})
})
describe('CYPRESS_LOCAL_STUDIO_PATH not set', () => {
let writeResult: string
let readStream: Readable
beforeEach(() => {
readStream = Readable.from('console.log("studio script")')
writeResult = ''
const writeStream = new Writable({
write: (chunk, encoding, callback) => {
writeResult += chunk.toString()
callback()
},
})
createWriteStreamStub.returns(writeStream)
createReadStreamStub.returns(Readable.from('tar contents'))
})
it('downloads the studio bundle and extracts it', async () => {
crossFetchStub.resolves({
body: readStream,
headers: {
get: (header) => {
if (header === 'x-cypress-signature') {
return '159'
}
},
},
})
verifySignatureFromFileStub.resolves(true)
const projectId = '12345'
await getAppStudio(projectId)
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/current.tgz', {
agent: sinon.match.any,
method: 'GET',
headers: {
'x-route-version': '1',
'x-cypress-signature': '1',
'x-cypress-project-slug': '12345',
'x-cypress-studio-mount-version': '1',
'x-os-name': 'linux',
'x-cypress-version': '1.2.3',
},
encrypt: 'signed',
})
expect(writeResult).to.eq('console.log("studio script")')
expect(verifySignatureFromFileStub).to.be.calledWith('/tmp/cypress/studio/bundle.tar', '159')
expect(extractStub).to.be.calledWith({
file: '/tmp/cypress/studio/bundle.tar',
cwd: '/tmp/cypress/studio',
})
expect(readFileStub).to.be.calledWith('/tmp/cypress/studio/server/index.js', 'utf8')
expect(studioManagerSetupStub).to.be.calledWithMatch({
script: 'console.log("studio script")',
studioPath: '/tmp/cypress/studio',
studioHash: 'V8T1PKuSTK1h9gr-1Z2Wtx__bxTpCXWRZ57sKmPVTSs',
})
})
it('downloads the studio bundle and extracts it after 1 fetch failure', async () => {
crossFetchStub.onFirstCall().rejects(new HttpError('Failed to fetch', 'url', 502, 'Bad Gateway', 'Bad Gateway', sinon.stub()))
crossFetchStub.onSecondCall().resolves({
body: readStream,
headers: {
get: (header) => {
if (header === 'x-cypress-signature') {
return '159'
}
},
},
})
verifySignatureFromFileStub.resolves(true)
const projectId = '12345'
await getAppStudio(projectId)
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/current.tgz', {
agent: sinon.match.any,
method: 'GET',
headers: {
'x-route-version': '1',
'x-cypress-signature': '1',
'x-cypress-project-slug': '12345',
'x-cypress-studio-mount-version': '1',
'x-os-name': 'linux',
'x-cypress-version': '1.2.3',
},
encrypt: 'signed',
})
expect(writeResult).to.eq('console.log("studio script")')
expect(verifySignatureFromFileStub).to.be.calledWith('/tmp/cypress/studio/bundle.tar', '159')
expect(extractStub).to.be.calledWith({
file: '/tmp/cypress/studio/bundle.tar',
cwd: '/tmp/cypress/studio',
})
expect(readFileStub).to.be.calledWith('/tmp/cypress/studio/server/index.js', 'utf8')
expect(studioManagerSetupStub).to.be.calledWithMatch({
script: 'console.log("studio script")',
studioPath: '/tmp/cypress/studio',
studioHash: 'V8T1PKuSTK1h9gr-1Z2Wtx__bxTpCXWRZ57sKmPVTSs',
})
})
it('throws an error and returns a studio manager in error state if the fetch fails more than twice', async () => {
const error = new HttpError('Failed to fetch', 'url', 502, 'Bad Gateway', 'Bad Gateway', sinon.stub())
crossFetchStub.rejects(error)
const projectId = '12345'
await getAppStudio(projectId)
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
expect(crossFetchStub).to.be.calledThrice
expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/current.tgz', {
agent: sinon.match.any,
method: 'GET',
headers: {
'x-route-version': '1',
'x-cypress-signature': '1',
'x-cypress-project-slug': '12345',
'x-cypress-studio-mount-version': '1',
'x-os-name': 'linux',
'x-cypress-version': '1.2.3',
},
encrypt: 'signed',
})
expect(createInErrorManagerStub).to.be.calledWithMatch(sinon.match.instanceOf(AggregateError))
})
it('throws an error and returns a studio manager in error state if the signature verification fails', async () => {
crossFetchStub.resolves({
body: readStream,
headers: {
get: (header) => {
if (header === 'x-cypress-signature') {
return '159'
}
},
},
})
verifySignatureFromFileStub.resolves(false)
const projectId = '12345'
await getAppStudio(projectId)
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
expect(writeResult).to.eq('console.log("studio script")')
expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/current.tgz', {
agent: sinon.match.any,
method: 'GET',
headers: {
'x-route-version': '1',
'x-cypress-signature': '1',
'x-cypress-project-slug': '12345',
'x-cypress-studio-mount-version': '1',
'x-os-name': 'linux',
'x-cypress-version': '1.2.3',
},
encrypt: 'signed',
})
expect(verifySignatureFromFileStub).to.be.calledWith('/tmp/cypress/studio/bundle.tar', '159')
expect(createInErrorManagerStub).to.be.calledWithMatch(sinon.match.instanceOf(Error).and(sinon.match.has('message', 'Unable to verify studio signature')))
})
it('throws an error if there is no signature in the response headers', async () => {
crossFetchStub.resolves({
body: readStream,
headers: {
get: () => null,
},
})
const projectId = '12345'
await getAppStudio(projectId)
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
expect(createInErrorManagerStub).to.be.calledWithMatch(sinon.match.instanceOf(Error).and(sinon.match.has('message', 'Unable to get studio signature')))
})
})
})

View File

@@ -3,6 +3,8 @@ const jose = require('jose')
const crypto = require('crypto')
const encryption = require('../../../lib/cloud/encryption')
const { expect } = require('chai')
const fs = require('fs')
const path = require('path')
const TEST_BODY = {
test: 'string',
@@ -63,4 +65,40 @@ describe('encryption', () => {
expect(roundtripResponse).to.eql(RESPONSE_BODY)
})
describe('verifySignatureFromFile', () => {
it('verifies a valid signature from a file', async () => {
const filePath = path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'encryption', 'index.js')
const fixtureContents = await fs.promises.readFile(filePath)
const sign = crypto.createSign('sha256', {
defaultEncoding: 'base64',
}).update(fixtureContents).end()
const signature = sign.sign(privateKey, 'base64')
expect(
await encryption.verifySignatureFromFile(
filePath,
signature,
publicKey,
),
).to.eql(true)
})
it('does not verify an invalid signature from a file', async () => {
const filePath = path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'encryption', 'index.js')
const fixtureContents = await fs.promises.readFile(filePath)
const sign = crypto.createSign('sha256', {
defaultEncoding: 'base64',
}).update(fixtureContents).end()
const signature = sign.sign(privateKey, 'base64')
expect(
await encryption.verifySignatureFromFile(
filePath,
`a ${signature}`,
publicKey,
),
).to.eql(false)
})
})
})

View File

@@ -37,7 +37,18 @@ describe('lib/cloud/protocol', () => {
beforeEach(async () => {
protocolManager = new ProtocolManager()
await protocolManager.setupProtocol(stubProtocol, { runId: '1', testingType: 'e2e', projectId: '1', cloudApi: { url: 'http://localhost:1234', retryWithBackoff: async () => {}, requestPromise: { get: async () => {} } } })
await protocolManager.setupProtocol(stubProtocol, {
runId: '1',
testingType: 'e2e',
projectId: '1',
cloudApi: { url: 'http://localhost:1234', retryWithBackoff: async () => {}, requestPromise: { get: async () => {} } },
projectConfig: {
devServerPublicPathRoute: '/__cypress-app/src',
namespace: '__cypress-app',
port: 1234,
proxyUrl: 'http://localhost:1234',
},
})
protocol = (protocolManager as any)._protocol
expect((protocol as any)).not.to.be.undefined

View File

@@ -0,0 +1,21 @@
import { expect } from 'chai'
import { requireScript } from '../../../lib/cloud/require_script'
describe('require_script', () => {
it('requires the script correctly', () => {
const script = `
module.exports = {
AppStudio: class {
constructor ({ studioPath }) {
this.studioPath = studioPath
}
}
}
`
const { AppStudio } = requireScript(script)
const studio = new AppStudio({ studioPath: '/path/to/studio' })
expect(studio.studioPath).to.equal('/path/to/studio')
})
})

View File

@@ -0,0 +1,115 @@
import { proxyquire, sinon } from '../../spec_helper'
import path from 'path'
import type { AppStudioShape } from '@packages/types'
import { expect } from 'chai'
import esbuild from 'esbuild'
import type { StudioManager as StudioManagerShape } from '@packages/server/lib/cloud/studio'
import os from 'os'
const pkg = require('@packages/root')
const { outputFiles: [{ contents: stubStudioRaw }] } = esbuild.buildSync({
entryPoints: [path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'studio', 'test-studio.ts')],
bundle: true,
format: 'cjs',
write: false,
platform: 'node',
})
const stubStudio = new TextDecoder('utf-8').decode(stubStudioRaw)
describe('lib/cloud/studio', () => {
let stubbedCrossFetch: sinon.SinonStub
let studioManager: StudioManagerShape
let studio: AppStudioShape
let StudioManager: typeof import('@packages/server/lib/cloud/studio').StudioManager
beforeEach(() => {
stubbedCrossFetch = sinon.stub()
StudioManager = (proxyquire('../lib/cloud/studio', {
'cross-fetch': stubbedCrossFetch,
}) as typeof import('@packages/server/lib/cloud/studio')).StudioManager
studioManager = new StudioManager()
studioManager.setup({ script: stubStudio, studioPath: 'path', studioHash: 'abcdefg' })
studio = (studioManager as any)._appStudio
sinon.stub(os, 'platform').returns('darwin')
sinon.stub(os, 'arch').returns('x64')
})
afterEach(() => {
sinon.restore()
})
describe('synchronous method invocation', () => {
it('reports an error when a synchronous method fails', async () => {
const error = new Error('foo')
sinon.stub(studio, 'initializeRoutes').throws(error)
await studioManager.initializeRoutes({} as any)
expect(studioManager.status).to.eq('IN_ERROR')
expect(stubbedCrossFetch).to.be.calledWithMatch(sinon.match((url: string) => url.endsWith('/studio/errors')), {
agent: sinon.match.any,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-cypress-version': pkg.version,
'x-os-name': 'darwin',
'x-arch': 'x64',
},
body: sinon.match((body) => {
const parsedBody = JSON.parse(body)
expect(parsedBody.studioHash).to.eq('abcdefg')
expect(parsedBody.errors[0].name).to.eq(error.name)
expect(parsedBody.errors[0].stack).to.eq(error.stack)
expect(parsedBody.errors[0].message).to.eq(error.message)
return true
}),
})
})
})
describe('createInErrorManager', () => {
it('creates a studio manager in error state', () => {
const manager = StudioManager.createInErrorManager(new Error('foo'))
expect(manager.status).to.eq('IN_ERROR')
expect(stubbedCrossFetch).to.be.calledWithMatch(sinon.match((url: string) => url.endsWith('/studio/errors')), {
agent: sinon.match.any,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-cypress-version': pkg.version,
'x-os-name': 'darwin',
'x-arch': 'x64',
},
body: sinon.match((body) => {
const parsedBody = JSON.parse(body)
expect(parsedBody.studioHash).to.be.undefined
expect(parsedBody.errors[0].name).to.eq('Error')
expect(parsedBody.errors[0].stack).to.be.a('string')
expect(parsedBody.errors[0].message).to.eq('foo')
return true
}),
})
})
})
describe('initializeRoutes', () => {
it('initializes routes', () => {
sinon.stub(studio, 'initializeRoutes')
const mockRouter = sinon.stub()
studioManager.initializeRoutes(mockRouter)
expect(studio.initializeRoutes).to.be.calledWith(mockRouter)
})
})
})

View File

@@ -13,6 +13,7 @@ const savedState = require(`../../lib/saved_state`)
const runEvents = require(`../../lib/plugins/run_events`)
const system = require(`../../lib/util/system`)
const { getCtx } = require(`../../lib/makeDataContext`)
const studio = require('../../lib/cloud/api/get_app_studio')
let ctx
@@ -33,6 +34,12 @@ describe('lib/project-base', () => {
sinon.stub(runEvents, 'execute').resolves()
this.testAppStudio = {
initializeRoutes: () => {},
}
sinon.stub(studio, 'getAppStudio').resolves(this.testAppStudio)
await ctx.actions.project.setCurrentProjectAndTestingTypeForTestSetup(this.todosPath)
this.config = await ctx.project.getConfig()
@@ -423,6 +430,30 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
})
it('gets app studio for the project id if CYPRESS_ENABLE_CLOUD_STUDIO is set', async function () {
process.env.CYPRESS_ENABLE_CLOUD_STUDIO = '1'
await this.project.open()
expect(studio.getAppStudio).to.be.calledWith('abc123')
expect(ctx.coreData.studio).to.eq(this.testAppStudio)
})
it('gets app studio for the project id if CYPRESS_LOCAL_STUDIO_PATH is set', async function () {
process.env.CYPRESS_LOCAL_STUDIO_PATH = '/path/to/app/studio'
await this.project.open()
expect(studio.getAppStudio).to.be.calledWith('abc123')
expect(ctx.coreData.studio).to.eq(this.testAppStudio)
})
it('does not get app studio if neither CYPRESS_ENABLE_CLOUD_STUDIO nor CYPRESS_LOCAL_STUDIO_PATH is set', async function () {
await this.project.open()
expect(studio.getAppStudio).not.to.be.called
expect(ctx.coreData.studio).to.be.null
})
describe('saved state', function () {
beforeEach(function () {
this._time = 1609459200000

View File

@@ -6,6 +6,8 @@ import chai, { expect } from 'chai'
import sinon from 'sinon'
import proxyquire from 'proxyquire'
import { Cfg } from '../../lib/project-base'
import '../spec_helper'
import { getCtx } from '@packages/data-context'
chai.use(require('@cypress/sinon-chai'))
@@ -46,7 +48,7 @@ describe('lib/routes', () => {
function setupCommonRoutes () {
const router = {
get: () => {},
get: sinon.stub(),
post: () => {},
all: () => {},
use: sinon.stub(),
@@ -204,5 +206,50 @@ describe('lib/routes', () => {
expect(next).to.be.called
})
it('initializes routes on studio if present', () => {
getCtx().coreData.studio = {
status: 'INITIALIZED',
initializeRoutes: sinon.stub(),
}
const { router } = setupCommonRoutes()
expect(getCtx().coreData.studio.initializeRoutes).to.be.calledWith(router)
})
it('initializes a dummy route for studio if studio is not present', () => {
const { router } = setupCommonRoutes()
const req = {
path: '/__cypress-studio/app-studio.js',
protocol: 'https',
}
const res = {
setHeader: sinon.stub(),
status: sinon.stub(),
send: sinon.stub(),
}
const next = sinon.stub().throws('next() should not be called')
res.status.returns(res)
router.get.withArgs('/__cypress-studio/app-studio.js').yield(req, res, next)
expect(res.setHeader).to.be.calledWith('Content-Type', 'application/javascript')
expect(res.status).to.be.calledWith(200)
expect(res.send).to.be.calledWith('')
})
it('does not initialize routes on studio if status is in error', () => {
getCtx().coreData.studio = {
status: 'IN_ERROR',
initializeRoutes: sinon.stub(),
}
setupCommonRoutes()
expect(getCtx().coreData.studio.initializeRoutes).not.to.be.called
})
})
})

View File

@@ -17,6 +17,7 @@
"@types/node": "20.16.0",
"better-sqlite3": "11.5.0",
"devtools-protocol": "0.0.1413303",
"express": "4.21.0",
"typescript": "~5.4.5"
},
"files": [

View File

@@ -0,0 +1,18 @@
import type { Router } from 'express'
export const STUDIO_STATUSES = ['NOT_INITIALIZED', 'INITIALIZED', 'IN_ERROR'] as const
export type StudioStatus = typeof STUDIO_STATUSES[number]
export interface StudioManagerShape extends AppStudioShape {
status: StudioStatus
}
export interface AppStudioShape {
initializeRoutes(router: Router): void
}
export type StudioErrorReport = {
studioHash?: string | null
errors: Error[]
}

View File

@@ -0,0 +1,11 @@
export type CloudApi = {
retryWithBackoff (fn: (attemptIndex: number) => Promise<any>): Promise<any>
requestPromise: {
get (options: any): Promise<any>
}
publicKeyVersion: string
enc: {
verifySignature (body: string, signature: string): boolean
}
baseUrl: string
}

View File

@@ -45,3 +45,7 @@ export * from './video'
export * from './protocol'
export * from './proxy'
export * from './cloud'
export * from './appStudio'

View File

@@ -6,7 +6,21 @@ const path = require('path')
const { setupV8Snapshots } = require('@tooling/v8-snapshot')
const { flipFuses, FuseVersion, FuseV1Options } = require('@electron/fuses')
const { buildEntryPointAndCleanup, cleanupUnneededDependencies } = require('./binary/binary-cleanup')
const { getIntegrityCheckSource, getBinaryEntryPointSource, getBinaryByteNodeEntryPointSource, getEncryptionFileSource, getCloudEnvironmentFileSource, validateEncryptionFile, getProtocolFileSource, validateCloudEnvironmentFile, validateProtocolFile, getIndexJscHash, DUMMY_INDEX_JSC_HASH } = require('./binary/binary-sources')
const {
getIntegrityCheckSource,
getBinaryEntryPointSource,
getBinaryByteNodeEntryPointSource,
getEncryptionFileSource,
getCloudEnvironmentFileSource,
validateEncryptionFile,
getProtocolFileSource,
validateCloudEnvironmentFile,
validateProtocolFile,
getStudioFileSource,
validateStudioFile,
getIndexJscHash,
DUMMY_INDEX_JSC_HASH,
} = require('./binary/binary-sources')
const verify = require('../cli/lib/tasks/verify')
const execa = require('execa')
const meta = require('./binary/meta')
@@ -81,12 +95,18 @@ module.exports = async function (params) {
const cloudApiFileSource = await getProtocolFileSource(cloudApiFilePath)
const cloudProtocolFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/protocol.ts')
const cloudProtocolFileSource = await getProtocolFileSource(cloudProtocolFilePath)
const projectBaseFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/project-base.ts')
const projectBaseFileSource = await getStudioFileSource(projectBaseFilePath)
const getAppStudioFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/api/get_app_studio.ts')
const getAppStudioFileSource = await getStudioFileSource(getAppStudioFilePath)
await Promise.all([
fs.writeFile(encryptionFilePath, encryptionFileSource),
fs.writeFile(cloudEnvironmentFilePath, cloudEnvironmentFileSource),
fs.writeFile(cloudApiFilePath, cloudApiFileSource),
fs.writeFile(cloudProtocolFilePath, cloudProtocolFileSource),
fs.writeFile(projectBaseFilePath, projectBaseFileSource),
fs.writeFile(getAppStudioFilePath, getAppStudioFileSource),
fs.writeFile(path.join(outputFolder, 'index.js'), binaryEntryPointSource),
])
@@ -99,6 +119,8 @@ module.exports = async function (params) {
validateCloudEnvironmentFile(cloudEnvironmentFilePath),
validateProtocolFile(cloudApiFilePath),
validateProtocolFile(cloudProtocolFilePath),
validateStudioFile(projectBaseFilePath),
validateStudioFile(getAppStudioFilePath),
])
await flipFuses(

View File

@@ -109,6 +109,16 @@ const getProtocolFileSource = async (protocolFilePath) => {
return fileContents.replaceAll('process.env.CYPRESS_LOCAL_PROTOCOL_PATH', 'undefined')
}
const getStudioFileSource = async (studioFilePath) => {
const fileContents = await fs.readFile(studioFilePath, 'utf8')
if (!fileContents.includes('process.env.CYPRESS_LOCAL_STUDIO_PATH')) {
throw new Error(`Expected to find CYPRESS_LOCAL_STUDIO_PATH in studio file`)
}
return fileContents.replaceAll('process.env.CYPRESS_LOCAL_STUDIO_PATH', 'undefined')
}
const validateProtocolFile = async (protocolFilePath) => {
const afterReplaceProtocol = await fs.readFile(protocolFilePath, 'utf8')
@@ -117,6 +127,14 @@ const validateProtocolFile = async (protocolFilePath) => {
}
}
const validateStudioFile = async (studioFilePath) => {
const afterReplaceStudio = await fs.readFile(studioFilePath, 'utf8')
if (afterReplaceStudio.includes('process.env.CYPRESS_LOCAL_STUDIO_PATH')) {
throw new Error(`Expected process.env.CYPRESS_LOCAL_STUDIO_PATH to be stripped from studio file`)
}
}
module.exports = {
getBinaryEntryPointSource,
getBinaryByteNodeEntryPointSource,
@@ -127,6 +145,8 @@ module.exports = {
validateCloudEnvironmentFile,
getProtocolFileSource,
validateProtocolFile,
getStudioFileSource,
validateStudioFile,
getIndexJscHash,
DUMMY_INDEX_JSC_HASH,
}

View File

@@ -60,7 +60,7 @@
"dockerode": "3.3.1",
"esbuild": "^0.15.3",
"execa": "4",
"express": "4.19.2",
"express": "4.21.0",
"express-session": "1.16.1",
"express-useragent": "1.0.15",
"fluent-ffmpeg": "2.1.2",

217
yarn.lock
View File

@@ -5174,6 +5174,47 @@
resolved "https://registry.yarnpkg.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d"
integrity sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==
"@module-federation/error-codes@0.8.9":
version "0.8.9"
resolved "https://registry.yarnpkg.com/@module-federation/error-codes/-/error-codes-0.8.9.tgz#28fa89b7502f4105ddbc767597edbddcccdf3ef0"
integrity sha512-yUA3GZjOy8Ll6l193faXir2veexDaUiLdmptbzC9tIee/iSQiSwIlibdTafCfqaJ62cLZaytOUdmAFAKLv8QQw==
"@module-federation/runtime-core@0.6.17":
version "0.6.17"
resolved "https://registry.yarnpkg.com/@module-federation/runtime-core/-/runtime-core-0.6.17.tgz#8dd7ccce44e85072b7ed90f456b1cca4b5915c79"
integrity sha512-PXFN/TT9f64Un6NQYqH1Z0QLhpytW15jkZvTEOV8W7Ed319BECFI0Rv4xAsAGa8zJGFoaM/c7QOQfdFXtKj5Og==
dependencies:
"@module-federation/error-codes" "0.8.9"
"@module-federation/sdk" "0.8.9"
"@module-federation/runtime@^0.8.0":
version "0.8.9"
resolved "https://registry.yarnpkg.com/@module-federation/runtime/-/runtime-0.8.9.tgz#e72623e888f387699a76633bd3bdb500cdb07374"
integrity sha512-i+a+/hoT/c+EE52mT+gJrbA6DhL86PY9cd/dIv/oKpLz9i+yYBlG+RA+puc7YsUEO4irbFLvnIMq6AGDUKVzYA==
dependencies:
"@module-federation/error-codes" "0.8.9"
"@module-federation/runtime-core" "0.6.17"
"@module-federation/sdk" "0.8.9"
"@module-federation/sdk@0.8.9":
version "0.8.9"
resolved "https://registry.yarnpkg.com/@module-federation/sdk/-/sdk-0.8.9.tgz#da0746610e8c470a4cf5ee094537ea6901a0aed9"
integrity sha512-QJ60itWC/SPjqduT7wDiF8UGwVU/yJ/Sz+QbnoxB9b7gNLzvI//swAXTo9eOtKsCy/V2BMwjt0F3eOcfnaqllA==
dependencies:
isomorphic-rslog "0.0.7"
"@module-federation/vite@^1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@module-federation/vite/-/vite-1.2.2.tgz#e8040ca5f7bc7e0e3633b88291ea140e791d625e"
integrity sha512-0RWkB8aE69BqjuZjl5Q1aITW3ntMSHh6jlBSe07ThDamQm1VBINU9VMkOhdjnhaTPCxjVnYG4lpnNb2dTvpdDA==
dependencies:
"@module-federation/runtime" "^0.8.0"
"@rollup/pluginutils" "^5.1.0"
defu "^6.1.4"
estree-walker "^2"
magic-string "^0.30.11"
pathe "^1.1.2"
"@n1ru4l/graphql-live-query@0.8.1":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@n1ru4l/graphql-live-query/-/graphql-live-query-0.8.1.tgz#2d6ca6157dafdc5d122a1aeb623b43e939c4b238"
@@ -8532,6 +8573,14 @@
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.7.tgz#545158342f949e8fd3bfd813224971ecddc3fac4"
integrity sha512-0VBprVqfgFD7Ehb2vd8Lh9TG3jP98gvr8rgehQqzztZNI7o8zS8Ad4jyZneKELphpuE212D8J70LnSNQSyO6bQ==
"@types/tar@^6.1.0":
version "6.1.13"
resolved "https://registry.yarnpkg.com/@types/tar/-/tar-6.1.13.tgz#9b5801c02175344101b4b91086ab2bbc8e93a9b6"
integrity sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==
dependencies:
"@types/node" "*"
minipass "^4.0.0"
"@types/through2@^2.0.36":
version "2.0.36"
resolved "https://registry.yarnpkg.com/@types/through2/-/through2-2.0.36.tgz#35fda0db635827d44c0e08e2c94653e647574a00"
@@ -14255,6 +14304,11 @@ defined@^1.0.0:
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
defu@^6.1.4:
version "6.1.4"
resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479"
integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==
degenerator@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5"
@@ -16074,7 +16128,7 @@ estree-walker@^1.0.1:
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
estree-walker@^2.0.1, estree-walker@^2.0.2:
estree-walker@^2, estree-walker@^2.0.1, estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
@@ -16371,43 +16425,6 @@ express-useragent@1.0.15:
resolved "https://registry.yarnpkg.com/express-useragent/-/express-useragent-1.0.15.tgz#cefda5fa4904345d51d3368b117a8dd4124985d9"
integrity sha512-eq5xMiYCYwFPoekffMjvEIk+NWdlQY9Y38OsTyl13IvA728vKT+q/CSERYWzcw93HGBJcIqMIsZC5CZGARPVdg==
express@4.19.2:
version "4.19.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
body-parser "1.20.2"
content-disposition "0.5.4"
content-type "~1.0.4"
cookie "0.6.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "2.0.0"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "1.2.0"
fresh "0.5.2"
http-errors "2.0.0"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "2.4.1"
parseurl "~1.3.3"
path-to-regexp "0.1.7"
proxy-addr "~2.0.7"
qs "6.11.0"
range-parser "~1.2.1"
safe-buffer "5.2.1"
send "0.18.0"
serve-static "1.15.0"
setprototypeof "1.2.0"
statuses "2.0.1"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
express@4.21.0:
version "4.21.0"
resolved "https://registry.yarnpkg.com/express/-/express-4.21.0.tgz#d57cb706d49623d4ac27833f1cbc466b668eb915"
@@ -16880,19 +16897,6 @@ fill-range@^7.1.1:
dependencies:
to-regex-range "^5.0.1"
finalhandler@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
escape-html "~1.0.3"
on-finished "2.4.1"
parseurl "~1.3.3"
statuses "2.0.1"
unpipe "~1.0.0"
finalhandler@1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019"
@@ -20262,6 +20266,11 @@ isomorphic-fetch@^3.0.0:
node-fetch "^2.6.1"
whatwg-fetch "^3.4.1"
isomorphic-rslog@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/isomorphic-rslog/-/isomorphic-rslog-0.0.7.tgz#e4e618b511a32f505e91ef28d9d5fb457ea1d45e"
integrity sha512-n6/XnKnZ5eLEj6VllG4XmamXG7/F69nls8dcynHyhcTpsPUYgcgx4ifEaCo4lQJ2uzwfmIT+F0KBGwBcMKmt5g==
isomorphic-ws@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
@@ -22286,11 +22295,6 @@ meow@^8.0.0, meow@^8.1.2:
type-fest "^0.18.0"
yargs-parser "^20.2.3"
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
merge-descriptors@1.0.3, merge-descriptors@~1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
@@ -22657,7 +22661,7 @@ minipass@^3.0.0, minipass@^3.1.1, minipass@^3.1.6:
dependencies:
yallist "^4.0.0"
minipass@^4.2.4:
minipass@^4.0.0, minipass@^4.2.4:
version "4.2.8"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a"
integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==
@@ -22782,7 +22786,7 @@ mobx@6.13.5:
resolved "https://registry.npmjs.org/mobx/-/mobx-6.13.5.tgz#957d9df88c7f8b4baa7c6f8bdcb6d68b432a6ed5"
integrity sha512-/HTWzW2s8J1Gqt+WmUj5Y0mddZk+LInejADc79NJadrWla3rHzmRHki/mnEUH1AvOmbNTZ1BRbKxr8DSgfdjMA==
"mocha-7.0.1@npm:mocha@7.0.1":
"mocha-7.0.1@npm:mocha@7.0.1", mocha@7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.0.1.tgz#276186d35a4852f6249808c6dd4a1376cbf6c6ce"
integrity sha512-9eWmWTdHLXh72rGrdZjNbG3aa1/3NRPpul1z0D979QpEnFdCG0Q5tv834N+94QEN2cysfV72YocQ3fn87s70fg==
@@ -22899,36 +22903,6 @@ mocha@6.2.2:
yargs-parser "13.1.1"
yargs-unparser "1.6.0"
mocha@7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.0.1.tgz#276186d35a4852f6249808c6dd4a1376cbf6c6ce"
integrity sha512-9eWmWTdHLXh72rGrdZjNbG3aa1/3NRPpul1z0D979QpEnFdCG0Q5tv834N+94QEN2cysfV72YocQ3fn87s70fg==
dependencies:
ansi-colors "3.2.3"
browser-stdout "1.3.1"
chokidar "3.3.0"
debug "3.2.6"
diff "3.5.0"
escape-string-regexp "1.0.5"
find-up "3.0.0"
glob "7.1.3"
growl "1.10.5"
he "1.2.0"
js-yaml "3.13.1"
log-symbols "2.2.0"
minimatch "3.0.4"
mkdirp "0.5.1"
ms "2.1.1"
node-environment-flags "1.0.6"
object.assign "4.1.0"
strip-json-comments "2.0.1"
supports-color "6.0.0"
which "1.3.1"
wide-align "1.1.3"
yargs "13.3.0"
yargs-parser "13.1.1"
yargs-unparser "1.6.0"
mocha@7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.1.0.tgz#c784f579ad0904d29229ad6cb1e2514e4db7d249"
@@ -25465,11 +25439,6 @@ path-to-regexp@0.1.10:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b"
integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-to-regexp@3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b"
@@ -28094,25 +28063,6 @@ semver@~2.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-2.3.2.tgz#b9848f25d6cf36333073ec9ef8856d42f1233e52"
integrity sha1-uYSPJdbPNjMwc+ye+IVtQvEjPlI=
send@0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==
dependencies:
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "2.0.0"
mime "1.6.0"
ms "2.1.3"
on-finished "2.4.1"
range-parser "~1.2.1"
statuses "2.0.1"
send@0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
@@ -28210,16 +28160,6 @@ serve-index@^1.9.1:
mime-types "~2.1.17"
parseurl "~1.3.2"
serve-static@1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.18.0"
serve-static@1.16.2:
version "1.16.2"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296"
@@ -29555,7 +29495,7 @@ string-template@~0.2.1:
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=
"string-width-cjs@npm:string-width@^4.2.0":
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -29581,15 +29521,6 @@ string-width@^1.0.1, string-width@^1.0.2:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^3.0.0, string-width@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
@@ -29700,7 +29631,7 @@ stringify-object@^3.0.0, stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -29721,13 +29652,6 @@ strip-ansi@5.2.0, strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@@ -30232,7 +30156,7 @@ tar@6.1.15:
mkdirp "^1.0.3"
yallist "^4.0.0"
tar@6.2.1, tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.1.2, tar@^6.2.1:
tar@6.2.1, tar@^6.0.5, tar@^6.1.0, tar@^6.1.11, tar@^6.1.12, tar@^6.1.2, tar@^6.2.1:
version "6.2.1"
resolved "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==
@@ -32849,7 +32773,7 @@ workerpool@6.2.0:
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b"
integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -32892,15 +32816,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"