mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-08 08:10:02 -06:00
chore: add ability to load types from the studio bundle (#31153)
* chore: set up sharing of react via module federation in studio * chore: add ability to load types from the studio bundle * fix build * fix build * fix build * PR comments * Update guides/studio-development.md Co-authored-by: Matt Schile <mschile@cypress.io> * fix test --------- Co-authored-by: Matt Schile <mschile@cypress.io>
This commit is contained in:
@@ -9,3 +9,17 @@ In production, the code used to facilitate Studio functionality will be retrieve
|
||||
- Clone the `cypress` repo
|
||||
- Run `yarn`
|
||||
- Run `yarn cypress:open`
|
||||
|
||||
## Types
|
||||
|
||||
The studio bundle provides the types for the `app` and `server` interfaces that are used within the Cypress code. To incorporate the types into the code base, run:
|
||||
|
||||
```sh
|
||||
yarn gulp downloadStudioTypes
|
||||
```
|
||||
|
||||
or to reference a local `cypress_services` repo:
|
||||
|
||||
```sh
|
||||
CYPRESS_LOCAL_STUDIO_PATH=<path-to-cypress-services/app/studio/dist/development-directory> yarn gulp downloadStudioTypes
|
||||
```
|
||||
|
||||
@@ -12,10 +12,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { init, loadRemote } from '@module-federation/runtime'
|
||||
import type { StudioAppDefaultShape, StudioPanelShape } from './studio-app-types'
|
||||
|
||||
interface StudioApp { default: StudioAppDefaultShape }
|
||||
|
||||
const root = ref<HTMLElement | null>(null)
|
||||
const error = ref<string | null>(null)
|
||||
const Panel = ref<ReturnType<any> | null>(null)
|
||||
const Panel = ref<StudioPanelShape | null>(null)
|
||||
|
||||
const maybeRenderReactComponent = () => {
|
||||
if (!Panel.value || !!error.value) {
|
||||
@@ -61,14 +64,14 @@ init({
|
||||
onMounted(maybeRenderReactComponent)
|
||||
onBeforeUnmount(unmountReactComponent)
|
||||
|
||||
loadRemote<typeof import('app-studio')>('app-studio').then((module) => {
|
||||
loadRemote<StudioApp>('app-studio').then((module) => {
|
||||
if (!module?.default) {
|
||||
error.value = 'The panel was not loaded successfully'
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Panel.value = module.default
|
||||
Panel.value = module.default.StudioPanel
|
||||
maybeRenderReactComponent()
|
||||
}).catch((e) => {
|
||||
error.value = e.message
|
||||
|
||||
7
packages/app/src/studio/index.d.ts
vendored
7
packages/app/src/studio/index.d.ts
vendored
@@ -1,7 +0,0 @@
|
||||
declare module 'components/StudioPanel' {
|
||||
export const StudioPanel: () => JSX.Element
|
||||
}
|
||||
declare module 'app-studio' {
|
||||
import { StudioPanel } from 'components/StudioPanel'
|
||||
export default StudioPanel
|
||||
}
|
||||
7
packages/app/src/studio/studio-app-types.ts
Normal file
7
packages/app/src/studio/studio-app-types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface StudioPanelShape {
|
||||
(): JSX.Element
|
||||
}
|
||||
|
||||
export interface StudioAppDefaultShape {
|
||||
StudioPanel: StudioPanelShape
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
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'
|
||||
@@ -11,17 +10,19 @@ import fetch from 'cross-fetch'
|
||||
import { agent } from '@packages/network'
|
||||
import { asyncRetry, linearDelay } from '../../util/async_retry'
|
||||
import { isRetryableError } from '../network/is_retryable_error'
|
||||
import { PUBLIC_KEY_VERSION } from '../constants'
|
||||
|
||||
const pkg = require('@packages/root')
|
||||
const routes = require('../routes')
|
||||
|
||||
const _delay = linearDelay(500)
|
||||
|
||||
const studioPath = path.join(os.tmpdir(), 'cypress', 'studio')
|
||||
export 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> => {
|
||||
const downloadStudioBundleToTempDirectory = async (projectId?: string): Promise<void> => {
|
||||
let responseSignature: string | null = null
|
||||
|
||||
await (asyncRetry(async () => {
|
||||
@@ -31,7 +32,7 @@ const downloadAppStudioBundleToTempDirectory = async (projectId?: string): Promi
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-route-version': '1',
|
||||
'x-cypress-signature': cloudApi.publicKeyVersion,
|
||||
'x-cypress-signature': PUBLIC_KEY_VERSION,
|
||||
...(projectId ? { 'x-cypress-project-slug': projectId } : {}),
|
||||
'x-cypress-studio-mount-version': '1',
|
||||
'x-os-name': os.platform(),
|
||||
@@ -87,40 +88,47 @@ const getTarHash = (): Promise<string> => {
|
||||
})
|
||||
}
|
||||
|
||||
export const getAppStudio = async (projectId?: string): Promise<StudioManager> => {
|
||||
export const retrieveAndExtractStudioBundle = async ({ projectId }: { projectId?: string } = {}): Promise<{ 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'))
|
||||
|
||||
return { studioHash: undefined }
|
||||
}
|
||||
|
||||
await downloadStudioBundleToTempDirectory(projectId)
|
||||
|
||||
const studioHash = await getTarHash()
|
||||
|
||||
await tar.extract({
|
||||
file: bundlePath,
|
||||
cwd: studioPath,
|
||||
})
|
||||
|
||||
return { studioHash }
|
||||
}
|
||||
|
||||
export const getAndInitializeStudioManager = async ({ projectId }: { projectId?: string } = {}): Promise<StudioManager> => {
|
||||
let script: string
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
const { studioHash } = await retrieveAndExtractStudioBundle({ projectId })
|
||||
|
||||
script = await readFile(serverFilePath, 'utf8')
|
||||
|
||||
const appStudio = new StudioManager()
|
||||
const studioManager = new StudioManager()
|
||||
|
||||
appStudio.setup({ script, studioPath, studioHash })
|
||||
studioManager.setup({ script, studioPath, studioHash })
|
||||
|
||||
return appStudio
|
||||
return studioManager
|
||||
} catch (error: unknown) {
|
||||
let actualError: Error
|
||||
|
||||
@@ -131,5 +139,7 @@ export const getAppStudio = async (projectId?: string): Promise<StudioManager> =
|
||||
}
|
||||
|
||||
return StudioManager.createInErrorManager(actualError)
|
||||
} finally {
|
||||
await fs.promises.rm(bundlePath, { force: true })
|
||||
}
|
||||
}
|
||||
@@ -23,13 +23,12 @@ import { fs } from '../../util/fs'
|
||||
import ProtocolManager from '../protocol'
|
||||
import type { ProjectBase } from '../../project-base'
|
||||
import type { AfterSpecDurations } from '@packages/types'
|
||||
import { PUBLIC_KEY_VERSION } from '../constants'
|
||||
|
||||
const THIRTY_SECONDS = humanInterval('30 seconds')
|
||||
const SIXTY_SECONDS = humanInterval('60 seconds')
|
||||
const TWO_MINUTES = humanInterval('2 minutes')
|
||||
|
||||
const PUBLIC_KEY_VERSION = '1'
|
||||
|
||||
const DELAYS: number[] = process.env.API_RETRY_INTERVALS
|
||||
? process.env.API_RETRY_INTERVALS.split(',').map(_.toNumber)
|
||||
: [THIRTY_SECONDS, SIXTY_SECONDS, TWO_MINUTES]
|
||||
@@ -692,6 +691,4 @@ export default {
|
||||
},
|
||||
|
||||
retryWithBackoff,
|
||||
|
||||
publicKeyVersion: PUBLIC_KEY_VERSION,
|
||||
}
|
||||
|
||||
1
packages/server/lib/cloud/constants.ts
Normal file
1
packages/server/lib/cloud/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const PUBLIC_KEY_VERSION = '1'
|
||||
@@ -69,7 +69,7 @@ export class ProtocolManager implements ProtocolManagerShape {
|
||||
|
||||
await fs.ensureDir(cypressProtocolDirectory)
|
||||
|
||||
const { AppCaptureProtocol } = requireScript(script)
|
||||
const { AppCaptureProtocol } = requireScript<{ AppCaptureProtocol }>(script)
|
||||
|
||||
this._protocol = new AppCaptureProtocol(options)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import Module from 'module'
|
||||
* @param script - string
|
||||
* @returns exports
|
||||
*/
|
||||
export const requireScript = (script: string) => {
|
||||
export const requireScript = <T>(script: string): T => {
|
||||
const mod = new Module('id', module)
|
||||
|
||||
mod.filename = ''
|
||||
@@ -15,5 +15,5 @@ export const requireScript = (script: string) => {
|
||||
|
||||
module.children.splice(module.children.indexOf(mod), 1)
|
||||
|
||||
return mod.exports
|
||||
return mod.exports as T
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AppStudioShape, StudioErrorReport, StudioManagerShape, StudioStatus } from '@packages/types'
|
||||
import type { StudioErrorReport, StudioManagerShape, StudioStatus, StudioServerDefaultShape, StudioServerShape } from '@packages/types'
|
||||
import type { Router } from 'express'
|
||||
import fetch from 'cross-fetch'
|
||||
import pkg from '@packages/root'
|
||||
@@ -7,12 +7,14 @@ import { agent } from '@packages/network'
|
||||
import Debug from 'debug'
|
||||
import { requireScript } from './require_script'
|
||||
|
||||
type StudioServer = { default: StudioServerDefaultShape }
|
||||
|
||||
const debug = Debug('cypress:server:studio')
|
||||
const routes = require('./routes')
|
||||
|
||||
export class StudioManager implements StudioManagerShape {
|
||||
status: StudioStatus = 'NOT_INITIALIZED'
|
||||
private _appStudio: AppStudioShape | undefined
|
||||
private _studioServer: StudioServerShape | undefined
|
||||
private _studioHash: string | undefined
|
||||
|
||||
static createInErrorManager (error: Error): StudioManager {
|
||||
@@ -26,15 +28,15 @@ export class StudioManager implements StudioManagerShape {
|
||||
}
|
||||
|
||||
setup ({ script, studioPath, studioHash }: { script: string, studioPath: string, studioHash?: string }): void {
|
||||
const { AppStudio } = requireScript(script)
|
||||
const { createStudioServer } = requireScript<StudioServer>(script).default
|
||||
|
||||
this._appStudio = new AppStudio({ studioPath })
|
||||
this._studioServer = createStudioServer({ studioPath })
|
||||
this._studioHash = studioHash
|
||||
this.status = 'INITIALIZED'
|
||||
}
|
||||
|
||||
initializeRoutes (router: Router): void {
|
||||
if (this._appStudio) {
|
||||
if (this._studioServer) {
|
||||
this.invokeSync('initializeRoutes', { isEssential: true }, router)
|
||||
}
|
||||
}
|
||||
@@ -70,16 +72,16 @@ export class StudioManager implements StudioManagerShape {
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstracts invoking a synchronous method on the AppStudio instance, so we can handle
|
||||
* Abstracts invoking a synchronous method on the StudioServer 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) {
|
||||
private invokeSync<K extends StudioServerSyncMethods> (method: K, { isEssential }: { isEssential: boolean }, ...args: Parameters<StudioServerShape[K]>): any | void {
|
||||
if (!this._studioServer) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
return this._appStudio[method].apply(this._appStudio, args)
|
||||
return this._studioServer[method].apply(this._studioServer, args)
|
||||
} catch (error: unknown) {
|
||||
let actualError: Error
|
||||
|
||||
@@ -96,17 +98,17 @@ export class StudioManager implements StudioManagerShape {
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstracts invoking a synchronous method on the AppStudio instance, so we can handle
|
||||
* Abstracts invoking a synchronous method on the StudioServer 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) {
|
||||
private async invokeAsync <K extends StudioServerAsyncMethods> (method: K, { isEssential }: { isEssential: boolean }, ...args: Parameters<StudioServerShape[K]>): Promise<ReturnType<StudioServerShape[K]> | undefined> {
|
||||
if (!this._studioServer) {
|
||||
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)
|
||||
return await this._studioServer[method].apply(this._studioServer, args)
|
||||
} catch (error: unknown) {
|
||||
let actualError: Error
|
||||
|
||||
@@ -127,10 +129,10 @@ export class StudioManager implements StudioManagerShape {
|
||||
}
|
||||
|
||||
// Helper types for invokeSync / invokeAsync
|
||||
type AppStudioSyncMethods = {
|
||||
[K in keyof AppStudioShape]: ReturnType<AppStudioShape[K]> extends Promise<any> ? never : K
|
||||
}[keyof AppStudioShape]
|
||||
type StudioServerSyncMethods = {
|
||||
[K in keyof StudioServerShape]: ReturnType<StudioServerShape[K]> extends Promise<any> ? never : K
|
||||
}[keyof StudioServerShape]
|
||||
|
||||
type AppStudioAsyncMethods = {
|
||||
[K in keyof AppStudioShape]: ReturnType<AppStudioShape[K]> extends Promise<any> ? K : never
|
||||
}[keyof AppStudioShape]
|
||||
type StudioServerAsyncMethods = {
|
||||
[K in keyof StudioServerShape]: ReturnType<StudioServerShape[K]> extends Promise<any> ? K : never
|
||||
}[keyof StudioServerShape]
|
||||
|
||||
@@ -25,7 +25,7 @@ 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'
|
||||
import { getAndInitializeStudioManager } from './cloud/api/get_and_initialize_studio_manager'
|
||||
|
||||
export interface Cfg extends ReceivedCypressOptions {
|
||||
projectId?: string
|
||||
@@ -154,12 +154,12 @@ export class ProjectBase extends EE {
|
||||
|
||||
this._server = new ServerBase(cfg)
|
||||
|
||||
let appStudio: StudioManagerShape | null
|
||||
let studioManager: StudioManagerShape | null
|
||||
|
||||
if (process.env.CYPRESS_ENABLE_CLOUD_STUDIO || process.env.CYPRESS_LOCAL_STUDIO_PATH) {
|
||||
appStudio = await getAppStudio(cfg.projectId)
|
||||
studioManager = await getAndInitializeStudioManager({ projectId: cfg.projectId })
|
||||
this.ctx.update((data) => {
|
||||
data.studio = appStudio
|
||||
data.studio = studioManager
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import type { AppStudioShape } from '@packages/types'
|
||||
import type { StudioServerShape, StudioServerDefaultShape } from '@packages/types'
|
||||
import type { Router } from 'express'
|
||||
|
||||
export class AppStudio implements AppStudioShape {
|
||||
class StudioServer implements StudioServerShape {
|
||||
initializeRoutes (router: Router): void {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const studioServerDefault: StudioServerDefaultShape = {
|
||||
createStudioServer (): StudioServer {
|
||||
return new StudioServer()
|
||||
},
|
||||
}
|
||||
|
||||
export default studioServerDefault
|
||||
|
||||
@@ -2,8 +2,8 @@ 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
|
||||
describe('getAndInitializeStudioManager', () => {
|
||||
let getAndInitializeStudioManager: typeof import('@packages/server/lib/cloud/api/get_and_initialize_studio_manager').getAndInitializeStudioManager
|
||||
let rmStub: sinon.SinonStub = sinon.stub()
|
||||
let ensureStub: sinon.SinonStub = sinon.stub()
|
||||
let copyStub: sinon.SinonStub = sinon.stub()
|
||||
@@ -30,7 +30,7 @@ describe('getAppStudio', () => {
|
||||
createInErrorManagerStub = sinon.stub()
|
||||
studioManagerSetupStub = sinon.stub()
|
||||
|
||||
getAppStudio = (proxyquire('../lib/cloud/api/get_app_studio', {
|
||||
getAndInitializeStudioManager = (proxyquire('../lib/cloud/api/get_and_initialize_studio_manager', {
|
||||
fs: {
|
||||
promises: {
|
||||
rm: rmStub.resolves(),
|
||||
@@ -63,7 +63,7 @@ describe('getAppStudio', () => {
|
||||
'@packages/root': {
|
||||
version: '1.2.3',
|
||||
},
|
||||
}) as typeof import('@packages/server/lib/cloud/api/get_app_studio')).getAppStudio
|
||||
}) as typeof import('@packages/server/lib/cloud/api/get_and_initialize_studio_manager')).getAndInitializeStudioManager
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -76,7 +76,7 @@ describe('getAppStudio', () => {
|
||||
})
|
||||
|
||||
it('gets the studio bundle from the path specified in the environment variable', async () => {
|
||||
await getAppStudio()
|
||||
await getAndInitializeStudioManager()
|
||||
|
||||
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
|
||||
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
|
||||
@@ -126,7 +126,7 @@ describe('getAppStudio', () => {
|
||||
|
||||
const projectId = '12345'
|
||||
|
||||
await getAppStudio(projectId)
|
||||
await getAndInitializeStudioManager({ projectId })
|
||||
|
||||
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
|
||||
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
|
||||
@@ -180,7 +180,7 @@ describe('getAppStudio', () => {
|
||||
|
||||
const projectId = '12345'
|
||||
|
||||
await getAppStudio(projectId)
|
||||
await getAndInitializeStudioManager({ projectId })
|
||||
|
||||
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
|
||||
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
|
||||
@@ -224,7 +224,7 @@ describe('getAppStudio', () => {
|
||||
|
||||
const projectId = '12345'
|
||||
|
||||
await getAppStudio(projectId)
|
||||
await getAndInitializeStudioManager({ projectId })
|
||||
|
||||
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
|
||||
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
|
||||
@@ -263,7 +263,7 @@ describe('getAppStudio', () => {
|
||||
|
||||
const projectId = '12345'
|
||||
|
||||
await getAppStudio(projectId)
|
||||
await getAndInitializeStudioManager({ projectId })
|
||||
|
||||
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
|
||||
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
|
||||
@@ -297,7 +297,7 @@ describe('getAppStudio', () => {
|
||||
|
||||
const projectId = '12345'
|
||||
|
||||
await getAppStudio(projectId)
|
||||
await getAndInitializeStudioManager({ projectId })
|
||||
|
||||
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
|
||||
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
|
||||
@@ -5,16 +5,16 @@ describe('require_script', () => {
|
||||
it('requires the script correctly', () => {
|
||||
const script = `
|
||||
module.exports = {
|
||||
AppStudio: class {
|
||||
StudioManager: class {
|
||||
constructor ({ studioPath }) {
|
||||
this.studioPath = studioPath
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const { AppStudio } = requireScript(script)
|
||||
const { StudioManager } = requireScript<{ StudioManager: any }>(script)
|
||||
|
||||
const studio = new AppStudio({ studioPath: '/path/to/studio' })
|
||||
const studio = new StudioManager({ studioPath: '/path/to/studio' })
|
||||
|
||||
expect(studio.studioPath).to.equal('/path/to/studio')
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { proxyquire, sinon } from '../../spec_helper'
|
||||
import path from 'path'
|
||||
import type { AppStudioShape } from '@packages/types'
|
||||
import type { StudioServerShape } from '@packages/types'
|
||||
import { expect } from 'chai'
|
||||
import esbuild from 'esbuild'
|
||||
import type { StudioManager as StudioManagerShape } from '@packages/server/lib/cloud/studio'
|
||||
@@ -20,7 +20,7 @@ const stubStudio = new TextDecoder('utf-8').decode(stubStudioRaw)
|
||||
describe('lib/cloud/studio', () => {
|
||||
let stubbedCrossFetch: sinon.SinonStub
|
||||
let studioManager: StudioManagerShape
|
||||
let studio: AppStudioShape
|
||||
let studio: StudioServerShape
|
||||
let StudioManager: typeof import('@packages/server/lib/cloud/studio').StudioManager
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -31,7 +31,7 @@ describe('lib/cloud/studio', () => {
|
||||
|
||||
studioManager = new StudioManager()
|
||||
studioManager.setup({ script: stubStudio, studioPath: 'path', studioHash: 'abcdefg' })
|
||||
studio = (studioManager as any)._appStudio
|
||||
studio = (studioManager as any)._studioServer
|
||||
|
||||
sinon.stub(os, 'platform').returns('darwin')
|
||||
sinon.stub(os, 'arch').returns('x64')
|
||||
|
||||
@@ -13,7 +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')
|
||||
const studio = require('../../lib/cloud/api/get_and_initialize_studio_manager')
|
||||
|
||||
let ctx
|
||||
|
||||
@@ -34,11 +34,11 @@ describe('lib/project-base', () => {
|
||||
|
||||
sinon.stub(runEvents, 'execute').resolves()
|
||||
|
||||
this.testAppStudio = {
|
||||
this.testStudioManager = {
|
||||
initializeRoutes: () => {},
|
||||
}
|
||||
|
||||
sinon.stub(studio, 'getAppStudio').resolves(this.testAppStudio)
|
||||
sinon.stub(studio, 'getAndInitializeStudioManager').resolves(this.testStudioManager)
|
||||
|
||||
await ctx.actions.project.setCurrentProjectAndTestingTypeForTestSetup(this.todosPath)
|
||||
this.config = await ctx.project.getConfig()
|
||||
@@ -430,27 +430,27 @@ 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 () {
|
||||
it('gets studio manager 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)
|
||||
expect(studio.getAndInitializeStudioManager).to.be.calledWith({ projectId: 'abc123' })
|
||||
expect(ctx.coreData.studio).to.eq(this.testStudioManager)
|
||||
})
|
||||
|
||||
it('gets app studio for the project id if CYPRESS_LOCAL_STUDIO_PATH is set', async function () {
|
||||
it('gets studio manager 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)
|
||||
expect(studio.getAndInitializeStudioManager).to.be.calledWith({ projectId: 'abc123' })
|
||||
expect(ctx.coreData.studio).to.eq(this.testStudioManager)
|
||||
})
|
||||
|
||||
it('does not get app studio if neither CYPRESS_ENABLE_CLOUD_STUDIO nor CYPRESS_LOCAL_STUDIO_PATH is set', async function () {
|
||||
it('does not get studio manager 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(studio.getAndInitializeStudioManager).not.to.be.called
|
||||
expect(ctx.coreData.studio).to.be.null
|
||||
})
|
||||
|
||||
|
||||
@@ -48,4 +48,4 @@ export * from './proxy'
|
||||
|
||||
export * from './cloud'
|
||||
|
||||
export * from './appStudio'
|
||||
export * from './studio'
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import type { Router } from 'express'
|
||||
import type { StudioServerShape } from './studio-server-types'
|
||||
|
||||
export * from './studio-server-types'
|
||||
|
||||
export const STUDIO_STATUSES = ['NOT_INITIALIZED', 'INITIALIZED', 'IN_ERROR'] as const
|
||||
|
||||
export type StudioStatus = typeof STUDIO_STATUSES[number]
|
||||
|
||||
export interface StudioManagerShape extends AppStudioShape {
|
||||
export interface StudioManagerShape extends StudioServerShape {
|
||||
status: StudioStatus
|
||||
}
|
||||
|
||||
export interface AppStudioShape {
|
||||
initializeRoutes(router: Router): void
|
||||
}
|
||||
|
||||
export type StudioErrorReport = {
|
||||
studioHash?: string | null
|
||||
errors: Error[]
|
||||
13
packages/types/src/studio/studio-server-types.ts
Normal file
13
packages/types/src/studio/studio-server-types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Router } from 'express'
|
||||
|
||||
export interface StudioServerOptions {
|
||||
studioPath: string
|
||||
}
|
||||
|
||||
export interface StudioServerShape {
|
||||
initializeRoutes(router: Router): void
|
||||
}
|
||||
|
||||
export interface StudioServerDefaultShape {
|
||||
createStudioServer: (options: StudioServerOptions) => StudioServerShape
|
||||
}
|
||||
@@ -97,8 +97,8 @@ module.exports = async function (params) {
|
||||
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)
|
||||
const getAndInitializeStudioManagerFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/api/get_and_initialize_studio_manager.ts')
|
||||
const getAndInitializeStudioManagerFileSource = await getStudioFileSource(getAndInitializeStudioManagerFilePath)
|
||||
|
||||
await Promise.all([
|
||||
fs.writeFile(encryptionFilePath, encryptionFileSource),
|
||||
@@ -106,7 +106,7 @@ module.exports = async function (params) {
|
||||
fs.writeFile(cloudApiFilePath, cloudApiFileSource),
|
||||
fs.writeFile(cloudProtocolFilePath, cloudProtocolFileSource),
|
||||
fs.writeFile(projectBaseFilePath, projectBaseFileSource),
|
||||
fs.writeFile(getAppStudioFilePath, getAppStudioFileSource),
|
||||
fs.writeFile(getAndInitializeStudioManagerFilePath, getAndInitializeStudioManagerFileSource),
|
||||
fs.writeFile(path.join(outputFolder, 'index.js'), binaryEntryPointSource),
|
||||
])
|
||||
|
||||
@@ -120,7 +120,7 @@ module.exports = async function (params) {
|
||||
validateProtocolFile(cloudApiFilePath),
|
||||
validateProtocolFile(cloudProtocolFilePath),
|
||||
validateStudioFile(projectBaseFilePath),
|
||||
validateStudioFile(getAppStudioFilePath),
|
||||
validateStudioFile(getAndInitializeStudioManagerFilePath),
|
||||
])
|
||||
|
||||
await flipFuses(
|
||||
|
||||
@@ -19,6 +19,7 @@ import { webpackReporter, webpackRunner } from './tasks/gulpWebpack'
|
||||
import { e2eTestScaffold, e2eTestScaffoldWatch } from './tasks/gulpE2ETestScaffold'
|
||||
import dedent from 'dedent'
|
||||
import { ensureCloudValidations, syncCloudValidations } from './tasks/gulpSyncValidations'
|
||||
import { downloadStudioTypes } from './tasks/gulpCloudDeliveredTypes'
|
||||
|
||||
if (process.env.CYPRESS_INTERNAL_VITE_DEV) {
|
||||
process.env.CYPRESS_INTERNAL_VITE_APP_PORT ??= '3333'
|
||||
@@ -255,6 +256,8 @@ gulp.task(startCypressWatch)
|
||||
gulp.task(openCypressApp)
|
||||
gulp.task(openCypressLaunchpad)
|
||||
|
||||
gulp.task(downloadStudioTypes)
|
||||
|
||||
// If we want to run individually, for debugging/testing
|
||||
gulp.task('cyOpenLaunchpadOnly', cyOpenLaunchpad)
|
||||
gulp.task('cyOpenAppOnly', cyOpenApp)
|
||||
|
||||
19
scripts/gulp/tasks/gulpCloudDeliveredTypes.ts
Normal file
19
scripts/gulp/tasks/gulpCloudDeliveredTypes.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
process.env.CYPRESS_INTERNAL_ENV = process.env.CYPRESS_INTERNAL_ENV ?? 'production'
|
||||
|
||||
import path from 'path'
|
||||
import fs from 'fs-extra'
|
||||
import { retrieveAndExtractStudioBundle, studioPath } from '@packages/server/lib/cloud/api/get_and_initialize_studio_manager'
|
||||
|
||||
export const downloadStudioTypes = async (): Promise<void> => {
|
||||
await retrieveAndExtractStudioBundle({ projectId: 'ypt4pf' })
|
||||
|
||||
await fs.copyFile(
|
||||
path.join(studioPath, 'app', 'types.ts'),
|
||||
path.join(__dirname, '..', '..', '..', 'packages', 'app', 'src', 'studio', 'studio-app-types.ts'),
|
||||
)
|
||||
|
||||
await fs.copyFile(
|
||||
path.join(studioPath, 'server', 'types.ts'),
|
||||
path.join(__dirname, '..', '..', '..', 'packages', 'types', 'src', 'studio', 'studio-server-types.ts'),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user