mirror of
https://github.com/cypress-io/cypress.git
synced 2026-01-10 00:30:37 -06:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
11
guides/studio-development.md
Normal file
11
guides/studio-development.md
Normal 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`
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
7
packages/app/src/studio/index.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
declare module 'app-studio' {
|
||||
export const mountTestGenerationPanel = (
|
||||
reactInstance: any,
|
||||
reactDOMInstance: any,
|
||||
container: HTMLElement,
|
||||
) => {}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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`,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
18
packages/graphql/src/schemaTypes/objectTypes/gql-Studio.ts
Normal file
18
packages/graphql/src/schemaTypes/objectTypes/gql-Studio.ts
Normal 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',
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -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'
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
135
packages/server/lib/cloud/api/get_app_studio.ts
Normal file
135
packages/server/lib/cloud/api/get_app_studio.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -692,4 +692,6 @@ export default {
|
||||
},
|
||||
|
||||
retryWithBackoff,
|
||||
|
||||
publicKeyVersion: PUBLIC_KEY_VERSION,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
19
packages/server/lib/cloud/require_script.ts
Normal file
19
packages/server/lib/cloud/require_script.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
136
packages/server/lib/cloud/studio.ts
Normal file
136
packages/server/lib/cloud/studio.ts
Normal 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]
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('studio script')
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { AppStudioShape } from '@packages/types'
|
||||
import type { Router } from 'express'
|
||||
|
||||
export class AppStudio implements AppStudioShape {
|
||||
initializeRoutes (router: Router): void {
|
||||
|
||||
}
|
||||
}
|
||||
307
packages/server/test/unit/cloud/api/get_app_studio_spec.ts
Normal file
307
packages/server/test/unit/cloud/api/get_app_studio_spec.ts
Normal 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')))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
21
packages/server/test/unit/cloud/require_script_spec.ts
Normal file
21
packages/server/test/unit/cloud/require_script_spec.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
115
packages/server/test/unit/cloud/studio_spec.ts
Normal file
115
packages/server/test/unit/cloud/studio_spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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": [
|
||||
|
||||
18
packages/types/src/appStudio.ts
Normal file
18
packages/types/src/appStudio.ts
Normal 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[]
|
||||
}
|
||||
11
packages/types/src/cloud.ts
Normal file
11
packages/types/src/cloud.ts
Normal 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
|
||||
}
|
||||
@@ -45,3 +45,7 @@ export * from './video'
|
||||
export * from './protocol'
|
||||
|
||||
export * from './proxy'
|
||||
|
||||
export * from './cloud'
|
||||
|
||||
export * from './appStudio'
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
217
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user