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:
Ryan Manuel
2025-02-28 16:30:45 -06:00
committed by GitHub
parent 78b372ae00
commit ffcc6387ef
22 changed files with 180 additions and 112 deletions

View File

@@ -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
```

View File

@@ -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

View File

@@ -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
}

View File

@@ -0,0 +1,7 @@
export interface StudioPanelShape {
(): JSX.Element
}
export interface StudioAppDefaultShape {
StudioPanel: StudioPanelShape
}

View File

@@ -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 })
}
}

View File

@@ -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,
}

View File

@@ -0,0 +1 @@
export const PUBLIC_KEY_VERSION = '1'

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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]

View File

@@ -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
})
}

View File

@@ -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

View File

@@ -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')

View File

@@ -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')
})

View File

@@ -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')

View File

@@ -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
})

View File

@@ -48,4 +48,4 @@ export * from './proxy'
export * from './cloud'
export * from './appStudio'
export * from './studio'

View File

@@ -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[]

View 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
}

View File

@@ -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(

View File

@@ -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)

View 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'),
)
}