mirror of
https://github.com/cypress-io/cypress.git
synced 2026-01-25 08:29:06 -06:00
refactor: shared routing for E2E and CT (#27618)
* remove server e2e * missing functions * fixing tests * minimize server-ct * linting * trying to fix * remove unused file * routes * update usage * remove routes * remove ct specific route * revert code
This commit is contained in:
@@ -20,7 +20,7 @@ import type { BrowserInstance } from './browsers/types'
|
||||
const debug = Debug('cypress:server:open_project')
|
||||
|
||||
export class OpenProject {
|
||||
private projectBase: ProjectBase<any> | null = null
|
||||
private projectBase: ProjectBase | null = null
|
||||
relaunchBrowser: (() => Promise<BrowserInstance | null>) = () => {
|
||||
throw new Error('bad relaunch')
|
||||
}
|
||||
@@ -243,7 +243,7 @@ export class OpenProject {
|
||||
|
||||
debug(`New url is ${newSpecUrl}`)
|
||||
|
||||
this.projectBase.server._socket.changeToUrl(newSpecUrl)
|
||||
this.projectBase.server.socket.changeToUrl(newSpecUrl)
|
||||
}
|
||||
|
||||
changeUrlToDebug (runNumber: number) {
|
||||
@@ -259,7 +259,7 @@ export class OpenProject {
|
||||
|
||||
debug(`New url is ${newUrl}`)
|
||||
|
||||
this.projectBase.server._socket.changeToUrl(newUrl)
|
||||
this.projectBase.server.socket.changeToUrl(newUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,7 +268,7 @@ export class OpenProject {
|
||||
* @returns
|
||||
*/
|
||||
updateTelemetryContext (context: string) {
|
||||
return this.projectBase?.server._socket.updateTelemetryContext(context)
|
||||
return this.projectBase?.server.socket.updateTelemetryContext(context)
|
||||
}
|
||||
|
||||
// close existing open project if it exists, for example
|
||||
|
||||
@@ -13,8 +13,6 @@ import preprocessor from './plugins/preprocessor'
|
||||
import runEvents from './plugins/run_events'
|
||||
import Reporter from './reporter'
|
||||
import * as savedState from './saved_state'
|
||||
import { ServerCt } from './server-ct'
|
||||
import { ServerE2E } from './server-e2e'
|
||||
import { SocketCt } from './socket-ct'
|
||||
import { SocketE2E } from './socket-e2e'
|
||||
import { ensureProp } from './util/class-helpers'
|
||||
@@ -23,11 +21,13 @@ import system from './util/system'
|
||||
import type { BannersState, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ReceivedCypressOptions, ResolvedConfigurationOptions, TestingType, VideoRecording } from '@packages/types'
|
||||
import { DataContext, getCtx } from '@packages/data-context'
|
||||
import { createHmac } from 'crypto'
|
||||
import { ServerBase } from './server-base'
|
||||
|
||||
export interface Cfg extends ReceivedCypressOptions {
|
||||
projectId?: string
|
||||
projectRoot: string
|
||||
proxyServer?: Cypress.RuntimeConfigOptions['proxyUrl']
|
||||
fileServerFolder?: Cypress.ResolvedConfigOptions['fileServerFolder']
|
||||
testingType: TestingType
|
||||
exit?: boolean
|
||||
state?: {
|
||||
@@ -48,15 +48,13 @@ const debug = Debug('cypress:server:project')
|
||||
|
||||
type StartWebsocketOptions = Pick<Cfg, 'socketIoCookie' | 'namespace' | 'screenshotsFolder' | 'report' | 'reporter' | 'reporterOptions' | 'projectRoot'>
|
||||
|
||||
export type Server = ServerE2E | ServerCt
|
||||
|
||||
export class ProjectBase<TServer extends Server> extends EE {
|
||||
export class ProjectBase extends EE {
|
||||
// id is sha256 of projectRoot
|
||||
public id: string
|
||||
|
||||
protected ctx: DataContext
|
||||
protected _cfg?: Cfg
|
||||
protected _server?: TServer
|
||||
protected _server?: ServerBase<any>
|
||||
protected _automation?: Automation
|
||||
private _recordTests?: any = null
|
||||
private _isServerOpen: boolean = false
|
||||
@@ -137,12 +135,6 @@ export class ProjectBase<TServer extends Server> extends EE {
|
||||
return this._server?.remoteStates
|
||||
}
|
||||
|
||||
createServer (testingType: Cypress.TestingType) {
|
||||
return testingType === 'e2e'
|
||||
? new ServerE2E() as TServer
|
||||
: new ServerCt() as TServer
|
||||
}
|
||||
|
||||
async open () {
|
||||
debug('opening project instance %s', this.projectRoot)
|
||||
debug('project open options %o', this.options)
|
||||
@@ -151,7 +143,7 @@ export class ProjectBase<TServer extends Server> extends EE {
|
||||
|
||||
process.chdir(this.projectRoot)
|
||||
|
||||
this._server = this.createServer(this.testingType)
|
||||
this._server = new ServerBase()
|
||||
|
||||
const [port, warning] = await this._server.open(cfg, {
|
||||
getCurrentBrowser: () => this.browser,
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import Debug from 'debug'
|
||||
import { Request, Response, Router } from 'express'
|
||||
import type { InitializeRoutes } from './routes'
|
||||
import send from 'send'
|
||||
import { getPathToDist } from '@packages/resolve-dist'
|
||||
|
||||
const debug = Debug('cypress:server:routes-ct')
|
||||
|
||||
const serveChunk = (req: Request, res: Response, clientRoute: string) => {
|
||||
let pathToFile = getPathToDist('runner', req.originalUrl.replace(clientRoute, ''))
|
||||
|
||||
return send(req, pathToFile).pipe(res)
|
||||
}
|
||||
|
||||
export const createRoutesCT = ({
|
||||
config,
|
||||
nodeProxy,
|
||||
}: InitializeRoutes) => {
|
||||
const routesCt = Router()
|
||||
|
||||
routesCt.get(`/${config.namespace}/static/*`, (req, res) => {
|
||||
debug(`proxying to %s/static, originalUrl %s`, config.namespace, req.originalUrl)
|
||||
const pathToFile = getPathToDist('static', req.params[0])
|
||||
|
||||
return send(req, pathToFile)
|
||||
.pipe(res)
|
||||
})
|
||||
|
||||
// user app code + spec code
|
||||
// default mounted to /__cypress/src/*
|
||||
routesCt.get(`${config.devServerPublicPathRoute}*`, (req, res) => {
|
||||
debug(`proxying to %s, originalUrl %s`, config.devServerPublicPathRoute, req.originalUrl)
|
||||
// user the node proxy here instead of the network proxy
|
||||
// to avoid the user accidentally intercepting and modifying
|
||||
// their own app.js files + spec.js files
|
||||
nodeProxy.web(req, res, {}, (e) => {
|
||||
if (e) {
|
||||
// eslint-disable-next-line
|
||||
debug('Proxy request error. This is likely the socket hangup issue, we can basically ignore this because the stream will automatically continue once the asset will be available', e)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const clientRoute = config.clientRoute
|
||||
|
||||
if (!clientRoute) {
|
||||
throw Error(`clientRoute is required. Received ${clientRoute}`)
|
||||
}
|
||||
|
||||
// enables runner to make a dynamic import
|
||||
routesCt.get([
|
||||
`${clientRoute}ctChunk-*`,
|
||||
`${clientRoute}vendors~ctChunk-*`,
|
||||
], (req, res) => {
|
||||
debug('Serving Cypress front-end chunk by requested URL:', req.url)
|
||||
|
||||
serveChunk(req, res, clientRoute)
|
||||
})
|
||||
|
||||
return routesCt
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import bodyParser from 'body-parser'
|
||||
import Debug from 'debug'
|
||||
import { Router } from 'express'
|
||||
import path from 'path'
|
||||
|
||||
import AppData from './util/app_data'
|
||||
import CacheBuster from './util/cache_buster'
|
||||
import specController from './controllers/spec'
|
||||
import reporter from './controllers/reporter'
|
||||
import client from './controllers/client'
|
||||
import files from './controllers/files'
|
||||
import type { InitializeRoutes } from './routes'
|
||||
import * as plugins from './plugins'
|
||||
import { privilegedCommandsManager } from './privileged-commands/privileged-commands-manager'
|
||||
|
||||
const debug = Debug('cypress:server:routes-e2e')
|
||||
|
||||
export const createRoutesE2E = ({
|
||||
config,
|
||||
networkProxy,
|
||||
onError,
|
||||
}: InitializeRoutes) => {
|
||||
const routesE2E = Router()
|
||||
|
||||
// routing for the actual specs which are processed automatically
|
||||
// this could be just a regular .js file or a .coffee file
|
||||
routesE2E.get(`/${config.namespace}/tests`, (req, res, next) => {
|
||||
// slice out the cache buster
|
||||
const test = CacheBuster.strip(req.query.p)
|
||||
|
||||
specController.handle(test, req, res, config, next, onError)
|
||||
})
|
||||
|
||||
routesE2E.post(`/${config.namespace}/process-origin-callback`, bodyParser.json(), async (req, res) => {
|
||||
try {
|
||||
const { file, fn, projectRoot } = req.body
|
||||
|
||||
debug('process origin callback: %s', fn)
|
||||
|
||||
const contents = await plugins.execute('_process:cross:origin:callback', { file, fn, projectRoot })
|
||||
|
||||
res.json({ contents })
|
||||
} catch (err) {
|
||||
const errorMessage = `Processing the origin callback errored:\n\n${err.stack}`
|
||||
|
||||
debug(errorMessage)
|
||||
|
||||
res.json({
|
||||
error: errorMessage,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
routesE2E.get(`/${config.namespace}/socket.io.js`, (req, res) => {
|
||||
client.handle(req, res)
|
||||
})
|
||||
|
||||
routesE2E.get(`/${config.namespace}/reporter/*`, (req, res) => {
|
||||
reporter.handle(req, res)
|
||||
})
|
||||
|
||||
routesE2E.get(`/${config.namespace}/automation/getLocalStorage`, (req, res) => {
|
||||
res.sendFile(path.join(__dirname, './html/get-local-storage.html'))
|
||||
})
|
||||
|
||||
routesE2E.get(`/${config.namespace}/automation/setLocalStorage`, (req, res) => {
|
||||
const origin = req.originalUrl.slice(req.originalUrl.indexOf('?') + 1)
|
||||
|
||||
networkProxy.http.getRenderedHTMLOrigins()[origin] = true
|
||||
|
||||
res.sendFile(path.join(__dirname, './html/set-local-storage.html'))
|
||||
})
|
||||
|
||||
routesE2E.get(`/${config.namespace}/source-maps/:id.map`, (req, res) => {
|
||||
networkProxy.handleSourceMapRequest(req, res)
|
||||
})
|
||||
|
||||
// special fallback - serve dist'd (bundled/static) files from the project path folder
|
||||
routesE2E.get(`/${config.namespace}/bundled/*`, (req, res) => {
|
||||
const file = AppData.getBundledFilePath(config.projectRoot, path.join('src', req.params[0]))
|
||||
|
||||
debug(`Serving dist'd bundle at file path: %o`, { path: file, url: req.url })
|
||||
|
||||
res.sendFile(file, { etag: false })
|
||||
})
|
||||
|
||||
// TODO: The below route is not technically correct for cypress in cypress tests.
|
||||
// We should be using 'config.namespace' to provide the namespace instead of hard coding __cypress, however,
|
||||
// In the runner when we create the spec bridge we have no knowledge of the namespace used by the server so
|
||||
// we create a spec bridge for the namespace of the server specified in the config, but that server hasn't been created.
|
||||
// To fix this I think we need to find a way to listen in the cypress in cypress server for routes from the server the
|
||||
// cypress instance thinks should exist, but that's outside the current scope.
|
||||
routesE2E.get('/__cypress/spec-bridge-iframes', (req, res) => {
|
||||
debug('handling cross-origin iframe for domain: %s', req.hostname)
|
||||
|
||||
// Chrome plans to make document.domain immutable in Chrome 109, with the default value
|
||||
// of the Origin-Agent-Cluster header becoming 'true'. We explicitly disable this header
|
||||
// in the spec-bridge-iframe to allow setting document.domain to the bare domain
|
||||
// to guarantee the spec bridge can communicate with the injected code.
|
||||
// @see https://github.com/cypress-io/cypress/issues/25010
|
||||
res.setHeader('Origin-Agent-Cluster', '?0')
|
||||
|
||||
files.handleCrossOriginIframe(req, res, config)
|
||||
})
|
||||
|
||||
routesE2E.post(`/${config.namespace}/add-verified-command`, bodyParser.json(), (req, res) => {
|
||||
privilegedCommandsManager.addVerifiedCommand(req.body)
|
||||
|
||||
res.sendStatus(204)
|
||||
})
|
||||
|
||||
return routesE2E
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import Debug from 'debug'
|
||||
import { ErrorRequestHandler, Request, Router } from 'express'
|
||||
import send from 'send'
|
||||
import { getPathToDist } from '@packages/resolve-dist'
|
||||
|
||||
import type { NetworkProxy } from '@packages/proxy'
|
||||
import type { Cfg } from './project-base'
|
||||
import xhrs from './controllers/xhrs'
|
||||
@@ -13,6 +12,16 @@ import type { FoundSpec } from '@packages/types'
|
||||
import { getCtx } from '@packages/data-context'
|
||||
import { graphQLHTTP } from '@packages/graphql/src/makeGraphQLServer'
|
||||
import type { RemoteStates } from './remote_states'
|
||||
import bodyParser from 'body-parser'
|
||||
import path from 'path'
|
||||
import AppData from './util/app_data'
|
||||
import CacheBuster from './util/cache_buster'
|
||||
import specController from './controllers/spec'
|
||||
import reporter from './controllers/reporter'
|
||||
import client from './controllers/client'
|
||||
import files from './controllers/files'
|
||||
import * as plugins from './plugins'
|
||||
import { privilegedCommandsManager } from './privileged-commands/privileged-commands-manager'
|
||||
|
||||
const debug = Debug('cypress:server:routes')
|
||||
|
||||
@@ -33,10 +42,96 @@ export const createCommonRoutes = ({
|
||||
getSpec,
|
||||
remoteStates,
|
||||
nodeProxy,
|
||||
onError,
|
||||
}: InitializeRoutes) => {
|
||||
const router = Router()
|
||||
const { clientRoute, namespace } = config
|
||||
|
||||
router.get(`/${config.namespace}/tests`, (req, res, next) => {
|
||||
// slice out the cache buster
|
||||
const test = CacheBuster.strip(req.query.p)
|
||||
|
||||
specController.handle(test, req, res, config, next, onError)
|
||||
})
|
||||
|
||||
router.post(`/${config.namespace}/process-origin-callback`, bodyParser.json(), async (req, res) => {
|
||||
try {
|
||||
const { file, fn, projectRoot } = req.body
|
||||
|
||||
debug('process origin callback: %s', fn)
|
||||
|
||||
const contents = await plugins.execute('_process:cross:origin:callback', { file, fn, projectRoot })
|
||||
|
||||
res.json({ contents })
|
||||
} catch (err) {
|
||||
const errorMessage = `Processing the origin callback errored:\n\n${err.stack}`
|
||||
|
||||
debug(errorMessage)
|
||||
|
||||
res.json({
|
||||
error: errorMessage,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
router.get(`/${config.namespace}/socket.io.js`, (req, res) => {
|
||||
client.handle(req, res)
|
||||
})
|
||||
|
||||
router.get(`/${config.namespace}/reporter/*`, (req, res) => {
|
||||
reporter.handle(req, res)
|
||||
})
|
||||
|
||||
router.get(`/${config.namespace}/automation/getLocalStorage`, (req, res) => {
|
||||
res.sendFile(path.join(__dirname, './html/get-local-storage.html'))
|
||||
})
|
||||
|
||||
router.get(`/${config.namespace}/automation/setLocalStorage`, (req, res) => {
|
||||
const origin = req.originalUrl.slice(req.originalUrl.indexOf('?') + 1)
|
||||
|
||||
networkProxy.http.getRenderedHTMLOrigins()[origin] = true
|
||||
|
||||
res.sendFile(path.join(__dirname, './html/set-local-storage.html'))
|
||||
})
|
||||
|
||||
router.get(`/${config.namespace}/source-maps/:id.map`, (req, res) => {
|
||||
networkProxy.handleSourceMapRequest(req, res)
|
||||
})
|
||||
|
||||
// special fallback - serve dist'd (bundled/static) files from the project path folder
|
||||
router.get(`/${config.namespace}/bundled/*`, (req, res) => {
|
||||
const file = AppData.getBundledFilePath(config.projectRoot, path.join('src', req.params[0]))
|
||||
|
||||
debug(`Serving dist'd bundle at file path: %o`, { path: file, url: req.url })
|
||||
|
||||
res.sendFile(file, { etag: false })
|
||||
})
|
||||
|
||||
// TODO: The below route is not technically correct for cypress in cypress tests.
|
||||
// We should be using 'config.namespace' to provide the namespace instead of hard coding __cypress, however,
|
||||
// In the runner when we create the spec bridge we have no knowledge of the namespace used by the server so
|
||||
// we create a spec bridge for the namespace of the server specified in the config, but that server hasn't been created.
|
||||
// To fix this I think we need to find a way to listen in the cypress in cypress server for routes from the server the
|
||||
// cypress instance thinks should exist, but that's outside the current scope.
|
||||
router.get('/__cypress/spec-bridge-iframes', (req, res) => {
|
||||
debug('handling cross-origin iframe for domain: %s', req.hostname)
|
||||
|
||||
// Chrome plans to make document.domain immutable in Chrome 109, with the default value
|
||||
// of the Origin-Agent-Cluster header becoming 'true'. We explicitly disable this header
|
||||
// in the spec-bridge-iframe to allow setting document.domain to the bare domain
|
||||
// to guarantee the spec bridge can communicate with the injected code.
|
||||
// @see https://github.com/cypress-io/cypress/issues/25010
|
||||
res.setHeader('Origin-Agent-Cluster', '?0')
|
||||
|
||||
files.handleCrossOriginIframe(req, res, config)
|
||||
})
|
||||
|
||||
router.post(`/${config.namespace}/add-verified-command`, bodyParser.json(), (req, res) => {
|
||||
privilegedCommandsManager.addVerifiedCommand(req.body)
|
||||
|
||||
res.sendStatus(204)
|
||||
})
|
||||
|
||||
if (process.env.CYPRESS_INTERNAL_VITE_DEV) {
|
||||
const proxy = httpProxy.createProxyServer({
|
||||
target: `http://localhost:${process.env.CYPRESS_INTERNAL_VITE_APP_PORT}/`,
|
||||
@@ -102,6 +197,24 @@ export const createCommonRoutes = ({
|
||||
return send(req, pathToFile).pipe(res)
|
||||
})
|
||||
|
||||
// user app code + spec code
|
||||
// default mounted to /__cypress/src/*
|
||||
// TODO: Remove this - only needed for Cy in Cy testing for unknown reasons.
|
||||
if (process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
|
||||
router.get(`${config.devServerPublicPathRoute}*`, (req, res) => {
|
||||
debug(`proxying to %s, originalUrl %s`, config.devServerPublicPathRoute, req.originalUrl)
|
||||
// user the node proxy here instead of the network proxy
|
||||
// to avoid the user accidentally intercepting and modifying
|
||||
// their own app.js files + spec.js files
|
||||
nodeProxy.web(req, res, {}, (e) => {
|
||||
if (e) {
|
||||
// eslint-disable-next-line
|
||||
debug('Proxy request error. This is likely the socket hangup issue, we can basically ignore this because the stream will automatically continue once the asset will be available', e)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
router.all('*', (req, res) => {
|
||||
networkProxy.handleHttpRequest(req, res)
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import compression from 'compression'
|
||||
import Debug from 'debug'
|
||||
import EventEmitter from 'events'
|
||||
import evilDns from 'evil-dns'
|
||||
import * as ensureUrl from './util/ensure-url'
|
||||
import express, { Express } from 'express'
|
||||
import http from 'http'
|
||||
import httpProxy from 'http-proxy'
|
||||
@@ -11,9 +12,9 @@ import _ from 'lodash'
|
||||
import type { AddressInfo } from 'net'
|
||||
import url from 'url'
|
||||
import la from 'lazy-ass'
|
||||
import type httpsProxy from '@packages/https-proxy'
|
||||
import { netStubbingState, NetStubbingState } from '@packages/net-stubbing'
|
||||
import { agent, clientCertificates, cors, httpUtils, uri } from '@packages/network'
|
||||
import httpsProxy from '@packages/https-proxy'
|
||||
import { getRoutesForRequest, netStubbingState, NetStubbingState } from '@packages/net-stubbing'
|
||||
import { agent, clientCertificates, cors, httpUtils, uri, concatStream } from '@packages/network'
|
||||
import { NetworkProxy, BrowserPreRequest } from '@packages/proxy'
|
||||
import type { SocketCt } from './socket-ct'
|
||||
import * as errors from './errors'
|
||||
@@ -27,16 +28,41 @@ import { createInitialWorkers } from '@packages/rewriter'
|
||||
import type { Cfg } from './project-base'
|
||||
import type { Browser } from '@packages/server/lib/browsers/types'
|
||||
import { InitializeRoutes, createCommonRoutes } from './routes'
|
||||
import { createRoutesE2E } from './routes-e2e'
|
||||
import { createRoutesCT } from './routes-ct'
|
||||
import type { FoundSpec } from '@packages/types'
|
||||
import type { FoundSpec, TestingType } from '@packages/types'
|
||||
import type { Server as WebSocketServer } from 'ws'
|
||||
import { RemoteStates } from './remote_states'
|
||||
import { cookieJar, SerializableAutomationCookie } from './util/cookies'
|
||||
import { resourceTypeAndCredentialManager, ResourceTypeAndCredentialManager } from './util/resourceTypeAndCredentialManager'
|
||||
import fileServer from './file_server'
|
||||
import appData from './util/app_data'
|
||||
import { graphqlWS } from '@packages/graphql/src/makeGraphQLServer'
|
||||
import statusCode from './util/status_code'
|
||||
import headersUtil from './util/headers'
|
||||
import stream from 'stream'
|
||||
import isHtml from 'is-html'
|
||||
|
||||
const debug = Debug('cypress:server:server-base')
|
||||
|
||||
const fullyQualifiedRe = /^https?:\/\//
|
||||
const htmlContentTypesRe = /^(text\/html|application\/xhtml)/i
|
||||
|
||||
const isResponseHtml = function (contentType, responseBuffer) {
|
||||
if (contentType) {
|
||||
// want to match anything starting with 'text/html'
|
||||
// including 'text/html;charset=utf-8' and 'Text/HTML'
|
||||
// https://github.com/cypress-io/cypress/issues/8506
|
||||
return htmlContentTypesRe.test(contentType)
|
||||
}
|
||||
|
||||
const body = _.invoke(responseBuffer, 'toString')
|
||||
|
||||
if (body) {
|
||||
return isHtml(body)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const _isNonProxiedRequest = (req) => {
|
||||
// proxied HTTP requests have a URL like: "http://example.com/foo"
|
||||
// non-proxied HTTP requests have a URL like: "/foo"
|
||||
@@ -111,7 +137,7 @@ export interface OpenServerOptions {
|
||||
shouldCorrelatePreRequests: () => boolean
|
||||
}
|
||||
|
||||
export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
export class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
private _middleware
|
||||
protected request: Request
|
||||
protected isListening: boolean
|
||||
@@ -129,6 +155,9 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
protected _eventBus: EventEmitter
|
||||
protected _remoteStates: RemoteStates
|
||||
private getCurrentBrowser: undefined | (() => Browser)
|
||||
private skipDomainInjectionForDomains: string[] | null = null
|
||||
private _urlResolver: Bluebird<Record<string, any>> | null = null
|
||||
private testingType?: TestingType
|
||||
|
||||
constructor () {
|
||||
this.isListening = false
|
||||
@@ -192,11 +221,90 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
this.socket.localBus.on('request:sent:with:credentials', this.resourceTypeAndCredentialManager.set)
|
||||
}
|
||||
|
||||
abstract createServer (
|
||||
createServer (
|
||||
app: Express,
|
||||
config: Cfg,
|
||||
onWarning: unknown,
|
||||
): Bluebird<[number, WarningErr?]>
|
||||
): Bluebird<[number, WarningErr?]> {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
const { port, fileServerFolder, socketIoRoute, baseUrl, experimentalSkipDomainInjection } = config
|
||||
|
||||
this._server = this._createHttpServer(app)
|
||||
|
||||
this.skipDomainInjectionForDomains = experimentalSkipDomainInjection
|
||||
|
||||
const onError = (err) => {
|
||||
// if the server bombs before starting
|
||||
// and the err no is EADDRINUSE
|
||||
// then we know to display the custom err message
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
return reject(this.portInUseErr(port))
|
||||
}
|
||||
}
|
||||
|
||||
debug('createServer connecting to server')
|
||||
|
||||
this.server.on('connect', this.onConnect.bind(this))
|
||||
this.server.on('upgrade', (req, socket, head) => this.onUpgrade(req, socket, head, socketIoRoute))
|
||||
this.server.once('error', onError)
|
||||
|
||||
this._graphqlWS = graphqlWS(this.server, `${socketIoRoute}-graphql`)
|
||||
|
||||
return this._listen(port, (err) => {
|
||||
// if the server bombs before starting
|
||||
// and the err no is EADDRINUSE
|
||||
// then we know to display the custom err message
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
return reject(this.portInUseErr(port))
|
||||
}
|
||||
})
|
||||
.then((port) => {
|
||||
return Bluebird.all([
|
||||
httpsProxy.create(appData.path('proxy'), port, {
|
||||
onRequest: this.callListeners.bind(this),
|
||||
onUpgrade: this.onSniUpgrade.bind(this),
|
||||
}),
|
||||
|
||||
fileServer.create(fileServerFolder),
|
||||
])
|
||||
.spread((httpsProxy, fileServer) => {
|
||||
this._httpsProxy = httpsProxy
|
||||
this._fileServer = fileServer
|
||||
|
||||
// if we have a baseUrl let's go ahead
|
||||
// and make sure the server is connectable!
|
||||
if (baseUrl) {
|
||||
this._baseUrl = baseUrl
|
||||
|
||||
if (config.isTextTerminal) {
|
||||
return this._retryBaseUrlCheck(baseUrl, onWarning)
|
||||
.return(null)
|
||||
.catch((e) => {
|
||||
debug(e)
|
||||
|
||||
return reject(errors.get('CANNOT_CONNECT_BASE_URL'))
|
||||
})
|
||||
}
|
||||
|
||||
return ensureUrl.isListening(baseUrl)
|
||||
.return(null)
|
||||
.catch((err) => {
|
||||
debug('ensuring baseUrl (%s) errored: %o', baseUrl, err)
|
||||
|
||||
return errors.get('CANNOT_CONNECT_BASE_URL_WARNING', baseUrl)
|
||||
})
|
||||
}
|
||||
}).then((warning) => {
|
||||
// once we open set the domain to root by default
|
||||
// which prevents a situation where navigating
|
||||
// to http sites redirects to /__/ cypress
|
||||
this._remoteStates.set(baseUrl != null ? baseUrl : '<root>')
|
||||
|
||||
return resolve([port, warning])
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
open (config: Cfg, {
|
||||
getSpec,
|
||||
@@ -209,11 +317,12 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
exit,
|
||||
}: OpenServerOptions) {
|
||||
debug('server open')
|
||||
this.testingType = testingType
|
||||
|
||||
la(_.isPlainObject(config), 'expected plain config object', config)
|
||||
|
||||
if (!config.baseUrl && testingType === 'component') {
|
||||
throw new Error('ServerCt#open called without config.baseUrl.')
|
||||
throw new Error('Server#open called without config.baseUrl.')
|
||||
}
|
||||
|
||||
const app = this.createExpressApp(config)
|
||||
@@ -253,11 +362,6 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
|
||||
this.setupCrossOriginRequestHandling()
|
||||
|
||||
const runnerSpecificRouter = testingType === 'e2e'
|
||||
? createRoutesE2E(routeOptions)
|
||||
: createRoutesCT(routeOptions)
|
||||
|
||||
app.use(runnerSpecificRouter)
|
||||
app.use(createCommonRoutes(routeOptions))
|
||||
|
||||
return this.createServer(app, config, onWarning)
|
||||
@@ -345,6 +449,9 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
}
|
||||
|
||||
startWebsockets (automation, config, options: Record<string, unknown> = {}) {
|
||||
// e2e only?
|
||||
options.onResolveUrl = this._onResolveUrl.bind(this)
|
||||
|
||||
options.onRequest = this._onRequest.bind(this)
|
||||
options.netStubbingState = this.netStubbingState
|
||||
options.getRenderedHTMLOrigins = this._networkProxy?.http.getRenderedHTMLOrigins
|
||||
@@ -574,4 +681,293 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
|
||||
return this.httpsProxy.connect(req, socket, head)
|
||||
}
|
||||
|
||||
_retryBaseUrlCheck (baseUrl, onWarning) {
|
||||
return ensureUrl.retryIsListening(baseUrl, {
|
||||
retryIntervals: [3000, 3000, 4000],
|
||||
onRetry ({ attempt, delay, remaining }) {
|
||||
const warning = errors.get('CANNOT_CONNECT_BASE_URL_RETRYING', {
|
||||
remaining,
|
||||
attempt,
|
||||
delay,
|
||||
baseUrl,
|
||||
})
|
||||
|
||||
return onWarning(warning)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
_onResolveUrl (urlStr, headers, automationRequest, options: Record<string, any> = { headers: {} }) {
|
||||
debug('resolving visit %o', {
|
||||
url: urlStr,
|
||||
headers,
|
||||
options,
|
||||
})
|
||||
|
||||
// always clear buffers - reduces the possibility of a random HTTP request
|
||||
// accidentally retrieving buffered content at the wrong time
|
||||
this._networkProxy?.reset()
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
// if we have an existing url resolver
|
||||
// in flight then cancel it
|
||||
if (this._urlResolver) {
|
||||
this._urlResolver.cancel()
|
||||
}
|
||||
|
||||
const request = this.request
|
||||
|
||||
let handlingLocalFile = false
|
||||
const previousRemoteState = this._remoteStates.current()
|
||||
const previousRemoteStateIsPrimary = this._remoteStates.isPrimarySuperDomainOrigin(previousRemoteState.origin)
|
||||
const primaryRemoteState = this._remoteStates.getPrimary()
|
||||
|
||||
// nuke any hashes from our url since
|
||||
// those those are client only and do
|
||||
// not apply to http requests
|
||||
urlStr = url.parse(urlStr)
|
||||
urlStr.hash = null
|
||||
urlStr = urlStr.format()
|
||||
|
||||
const originalUrl = urlStr
|
||||
|
||||
let reqStream = null
|
||||
let currentPromisePhase = null
|
||||
|
||||
const runPhase = (fn) => {
|
||||
return currentPromisePhase = fn()
|
||||
}
|
||||
|
||||
const matchesNetStubbingRoute = (requestOptions) => {
|
||||
const proxiedReq = {
|
||||
proxiedUrl: requestOptions.url,
|
||||
resourceType: 'document',
|
||||
..._.pick(requestOptions, ['headers', 'method']),
|
||||
// TODO: add `body` here once bodies can be statically matched
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const iterator = getRoutesForRequest(this.netStubbingState?.routes, proxiedReq)
|
||||
// If the iterator is exhausted (done) on the first try, then 0 matches were found
|
||||
const zeroMatches = iterator.next().done
|
||||
|
||||
return !zeroMatches
|
||||
}
|
||||
|
||||
let p
|
||||
|
||||
return this._urlResolver = (p = new Bluebird<Record<string, any>>((resolve, reject, onCancel) => {
|
||||
let urlFile
|
||||
|
||||
onCancel?.(() => {
|
||||
p.currentPromisePhase = currentPromisePhase
|
||||
p.reqStream = reqStream
|
||||
|
||||
_.invoke(reqStream, 'abort')
|
||||
|
||||
return _.invoke(currentPromisePhase, 'cancel')
|
||||
})
|
||||
|
||||
const redirects: any[] = []
|
||||
let newUrl: string | null = null
|
||||
|
||||
if (!fullyQualifiedRe.test(urlStr)) {
|
||||
handlingLocalFile = true
|
||||
|
||||
options.headers['x-cypress-authorization'] = this._fileServer?.token
|
||||
|
||||
const state = this._remoteStates.set(urlStr, options)
|
||||
|
||||
// TODO: Update url.resolve signature to not use deprecated methods
|
||||
urlFile = url.resolve(state.fileServer as string, urlStr)
|
||||
urlStr = url.resolve(state.origin as string, urlStr)
|
||||
}
|
||||
|
||||
const onReqError = (err) => {
|
||||
// only restore the previous state
|
||||
// if our promise is still pending
|
||||
if (p.isPending()) {
|
||||
restorePreviousRemoteState(previousRemoteState, previousRemoteStateIsPrimary)
|
||||
}
|
||||
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
const onReqStreamReady = (str) => {
|
||||
reqStream = str
|
||||
|
||||
return str
|
||||
.on('error', onReqError)
|
||||
.on('response', (incomingRes) => {
|
||||
debug(
|
||||
'resolve:url headers received, buffering response %o',
|
||||
_.pick(incomingRes, 'headers', 'statusCode'),
|
||||
)
|
||||
|
||||
if (newUrl == null) {
|
||||
newUrl = urlStr
|
||||
}
|
||||
|
||||
return runPhase(() => {
|
||||
// get the cookies that would be sent with this request so they can be rehydrated
|
||||
return automationRequest('get:cookies', {
|
||||
domain: cors.getSuperDomain(newUrl),
|
||||
})
|
||||
.then((cookies) => {
|
||||
const statusIs2xxOrAllowedFailure = () => {
|
||||
// is our status code in the 2xx range, or have we disabled failing
|
||||
// on status code?
|
||||
return statusCode.isOk(incomingRes.statusCode) || options.failOnStatusCode === false
|
||||
}
|
||||
|
||||
const isOk = statusIs2xxOrAllowedFailure()
|
||||
const contentType = headersUtil.getContentType(incomingRes)
|
||||
|
||||
const details: Record<string, unknown> = {
|
||||
isOkStatusCode: isOk,
|
||||
contentType,
|
||||
url: newUrl,
|
||||
status: incomingRes.statusCode,
|
||||
cookies,
|
||||
statusText: statusCode.getText(incomingRes.statusCode),
|
||||
redirects,
|
||||
originalUrl,
|
||||
}
|
||||
|
||||
// does this response have this cypress header?
|
||||
const fp = incomingRes.headers['x-cypress-file-path']
|
||||
|
||||
if (fp) {
|
||||
// if so we know this is a local file request
|
||||
details.filePath = fp
|
||||
}
|
||||
|
||||
debug('setting details resolving url %o', details)
|
||||
|
||||
const concatStr = concatStream((responseBuffer) => {
|
||||
// buffer the entire response before resolving.
|
||||
// this allows us to detect & reject ETIMEDOUT errors
|
||||
// where the headers have been sent but the
|
||||
// connection hangs before receiving a body.
|
||||
|
||||
// if there is not a content-type, try to determine
|
||||
// if the response content is HTML-like
|
||||
// https://github.com/cypress-io/cypress/issues/1727
|
||||
details.isHtml = isResponseHtml(contentType, responseBuffer)
|
||||
|
||||
debug('resolve:url response ended, setting buffer %o', { newUrl, details })
|
||||
|
||||
details.totalTime = Date.now() - startTime
|
||||
|
||||
// buffer the response and set the remote state if this is a successful html response
|
||||
// TODO: think about moving this logic back into the frontend so that the driver can be in control
|
||||
// of when to buffer and set the remote state
|
||||
if (isOk && details.isHtml) {
|
||||
const urlDoesNotMatchPolicyBasedOnDomain = options.hasAlreadyVisitedUrl
|
||||
&& !cors.urlMatchesPolicyBasedOnDomain(primaryRemoteState.origin, newUrl || '', { skipDomainInjectionForDomains: this.skipDomainInjectionForDomains })
|
||||
|| options.isFromSpecBridge
|
||||
|
||||
if (!handlingLocalFile) {
|
||||
this._remoteStates.set(newUrl as string, options, !urlDoesNotMatchPolicyBasedOnDomain)
|
||||
}
|
||||
|
||||
const responseBufferStream = new stream.PassThrough({
|
||||
highWaterMark: Number.MAX_SAFE_INTEGER,
|
||||
})
|
||||
|
||||
responseBufferStream.end(responseBuffer)
|
||||
|
||||
this._networkProxy?.setHttpBuffer({
|
||||
url: newUrl,
|
||||
stream: responseBufferStream,
|
||||
details,
|
||||
originalUrl,
|
||||
response: incomingRes,
|
||||
urlDoesNotMatchPolicyBasedOnDomain,
|
||||
})
|
||||
} else {
|
||||
// TODO: move this logic to the driver too for
|
||||
// the same reasons listed above
|
||||
restorePreviousRemoteState(previousRemoteState, previousRemoteStateIsPrimary)
|
||||
}
|
||||
|
||||
details.isPrimarySuperDomainOrigin = this._remoteStates.isPrimarySuperDomainOrigin(newUrl!)
|
||||
|
||||
return resolve(details)
|
||||
})
|
||||
|
||||
return str.pipe(concatStr)
|
||||
}).catch(onReqError)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const restorePreviousRemoteState = (previousRemoteState: Cypress.RemoteState, previousRemoteStateIsPrimary: boolean) => {
|
||||
this._remoteStates.set(previousRemoteState, {}, previousRemoteStateIsPrimary)
|
||||
}
|
||||
|
||||
// if they're POSTing an object, querystringify their POST body
|
||||
if ((options.method === 'POST') && _.isObject(options.body)) {
|
||||
options.form = options.body
|
||||
delete options.body
|
||||
}
|
||||
|
||||
_.assign(options, {
|
||||
// turn off gzip since we need to eventually
|
||||
// rewrite these contents
|
||||
gzip: false,
|
||||
url: urlFile != null ? urlFile : urlStr,
|
||||
headers: _.assign({
|
||||
accept: 'text/html,*/*',
|
||||
}, options.headers),
|
||||
onBeforeReqInit: runPhase,
|
||||
followRedirect (incomingRes) {
|
||||
const status = incomingRes.statusCode
|
||||
const next = incomingRes.headers.location
|
||||
|
||||
const curr = newUrl != null ? newUrl : urlStr
|
||||
|
||||
newUrl = url.resolve(curr, next)
|
||||
|
||||
redirects.push([status, newUrl].join(': '))
|
||||
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
||||
if (matchesNetStubbingRoute(options)) {
|
||||
// TODO: this is being used to force cy.visits to be interceptable by network stubbing
|
||||
// however, network errors will be obsfucated by the proxying so this is not an ideal solution
|
||||
_.merge(options, {
|
||||
proxy: `http://127.0.0.1:${this._port()}`,
|
||||
agent: null,
|
||||
headers: {
|
||||
'x-cypress-resolving-url': '1',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
debug('sending request with options %o', options)
|
||||
|
||||
return runPhase(() => {
|
||||
// @ts-ignore
|
||||
return request.sendStream(headers, automationRequest, options)
|
||||
.then((createReqStream) => {
|
||||
const stream = createReqStream()
|
||||
|
||||
return onReqStreamReady(stream)
|
||||
}).catch(onReqError)
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
destroyAut () {
|
||||
if (this.testingType === 'component' && 'destroyAut' in this.socket) {
|
||||
return this.socket.destroyAut()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import Bluebird from 'bluebird'
|
||||
import httpsProxy from '@packages/https-proxy'
|
||||
import { OpenServerOptions, ServerBase } from '@packages/server/lib/server-base'
|
||||
import appData from '@packages/server/lib/util/app_data'
|
||||
import type { SocketCt } from './socket-ct'
|
||||
import type { Cfg } from '@packages/server/lib/project-base'
|
||||
import { graphqlWS } from '@packages/graphql/src/makeGraphQLServer'
|
||||
|
||||
type WarningErr = Record<string, any>
|
||||
|
||||
export class ServerCt extends ServerBase<SocketCt> {
|
||||
open (config: Cfg, options: OpenServerOptions) {
|
||||
return super.open(config, { ...options, testingType: 'component' })
|
||||
}
|
||||
|
||||
createServer (app, config, onWarning): Bluebird<[number, WarningErr?]> {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
const { port, baseUrl, socketIoRoute } = config
|
||||
|
||||
this._server = this._createHttpServer(app)
|
||||
this.server.on('connect', this.onConnect.bind(this))
|
||||
this.server.on('upgrade', (req, socket, head) => this.onUpgrade(req, socket, head, socketIoRoute))
|
||||
|
||||
this._graphqlWS = graphqlWS(this.server, `${socketIoRoute}-graphql`)
|
||||
|
||||
return this._listen(port, (err) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
reject(`Port ${port} is already in use`)
|
||||
}
|
||||
|
||||
reject(err)
|
||||
})
|
||||
.then((port) => {
|
||||
httpsProxy.create(appData.path('proxy'), port, {
|
||||
onRequest: this.callListeners.bind(this),
|
||||
onUpgrade: this.onSniUpgrade.bind(this),
|
||||
})
|
||||
.then((httpsProxy) => {
|
||||
this._httpsProxy = httpsProxy
|
||||
|
||||
// once we open set the domain to root by default
|
||||
// which prevents a situation where navigating
|
||||
// to http sites redirects to /__/ cypress
|
||||
this._remoteStates.set(baseUrl)
|
||||
|
||||
return resolve([port])
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
destroyAut () {
|
||||
return this.socket.destroyAut()
|
||||
}
|
||||
}
|
||||
@@ -1,430 +0,0 @@
|
||||
import Bluebird from 'bluebird'
|
||||
import Debug from 'debug'
|
||||
import isHtml from 'is-html'
|
||||
import _ from 'lodash'
|
||||
import stream from 'stream'
|
||||
import url from 'url'
|
||||
import httpsProxy from '@packages/https-proxy'
|
||||
import { getRoutesForRequest } from '@packages/net-stubbing'
|
||||
import { concatStream, cors } from '@packages/network'
|
||||
import { graphqlWS } from '@packages/graphql/src/makeGraphQLServer'
|
||||
|
||||
import * as errors from './errors'
|
||||
import fileServer from './file_server'
|
||||
import { OpenServerOptions, ServerBase } from './server-base'
|
||||
import type { SocketE2E } from './socket-e2e'
|
||||
import appData from './util/app_data'
|
||||
import * as ensureUrl from './util/ensure-url'
|
||||
import headersUtil from './util/headers'
|
||||
import statusCode from './util/status_code'
|
||||
import type { Cfg } from './project-base'
|
||||
|
||||
type WarningErr = Record<string, any>
|
||||
|
||||
const fullyQualifiedRe = /^https?:\/\//
|
||||
const htmlContentTypesRe = /^(text\/html|application\/xhtml)/i
|
||||
|
||||
const debug = Debug('cypress:server:server-e2e')
|
||||
|
||||
const isResponseHtml = function (contentType, responseBuffer) {
|
||||
if (contentType) {
|
||||
// want to match anything starting with 'text/html'
|
||||
// including 'text/html;charset=utf-8' and 'Text/HTML'
|
||||
// https://github.com/cypress-io/cypress/issues/8506
|
||||
return htmlContentTypesRe.test(contentType)
|
||||
}
|
||||
|
||||
const body = _.invoke(responseBuffer, 'toString')
|
||||
|
||||
if (body) {
|
||||
return isHtml(body)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export class ServerE2E extends ServerBase<SocketE2E> {
|
||||
private _urlResolver: Bluebird<Record<string, any>> | null
|
||||
// the initialization of this variable is only precautionary as the actual config value is applied when the server is created
|
||||
private skipDomainInjectionForDomains: string[] | null = null
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this._urlResolver = null
|
||||
}
|
||||
|
||||
open (config: Cfg, options: OpenServerOptions) {
|
||||
return super.open(config, { ...options, testingType: 'e2e' })
|
||||
}
|
||||
|
||||
createServer (app, config, onWarning): Bluebird<[number, WarningErr?]> {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
const { port, fileServerFolder, socketIoRoute, baseUrl, experimentalSkipDomainInjection } = config
|
||||
|
||||
this._server = this._createHttpServer(app)
|
||||
this.skipDomainInjectionForDomains = experimentalSkipDomainInjection
|
||||
const onError = (err) => {
|
||||
// if the server bombs before starting
|
||||
// and the err no is EADDRINUSE
|
||||
// then we know to display the custom err message
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
return reject(this.portInUseErr(port))
|
||||
}
|
||||
}
|
||||
|
||||
debug('createServer connecting to server')
|
||||
|
||||
this.server.on('connect', this.onConnect.bind(this))
|
||||
this.server.on('upgrade', (req, socket, head) => this.onUpgrade(req, socket, head, socketIoRoute))
|
||||
this.server.once('error', onError)
|
||||
|
||||
this._graphqlWS = graphqlWS(this.server, `${socketIoRoute}-graphql`)
|
||||
|
||||
return this._listen(port, (err) => {
|
||||
// if the server bombs before starting
|
||||
// and the err no is EADDRINUSE
|
||||
// then we know to display the custom err message
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
return reject(this.portInUseErr(port))
|
||||
}
|
||||
})
|
||||
.then((port) => {
|
||||
return Bluebird.all([
|
||||
httpsProxy.create(appData.path('proxy'), port, {
|
||||
onRequest: this.callListeners.bind(this),
|
||||
onUpgrade: this.onSniUpgrade.bind(this),
|
||||
}),
|
||||
|
||||
fileServer.create(fileServerFolder),
|
||||
])
|
||||
.spread((httpsProxy, fileServer) => {
|
||||
this._httpsProxy = httpsProxy
|
||||
this._fileServer = fileServer
|
||||
|
||||
// if we have a baseUrl let's go ahead
|
||||
// and make sure the server is connectable!
|
||||
if (baseUrl) {
|
||||
this._baseUrl = baseUrl
|
||||
|
||||
if (config.isTextTerminal) {
|
||||
return this._retryBaseUrlCheck(baseUrl, onWarning)
|
||||
.return(null)
|
||||
.catch((e) => {
|
||||
debug(e)
|
||||
|
||||
return reject(errors.get('CANNOT_CONNECT_BASE_URL'))
|
||||
})
|
||||
}
|
||||
|
||||
return ensureUrl.isListening(baseUrl)
|
||||
.return(null)
|
||||
.catch((err) => {
|
||||
debug('ensuring baseUrl (%s) errored: %o', baseUrl, err)
|
||||
|
||||
return errors.get('CANNOT_CONNECT_BASE_URL_WARNING', baseUrl)
|
||||
})
|
||||
}
|
||||
}).then((warning) => {
|
||||
// once we open set the domain to root by default
|
||||
// which prevents a situation where navigating
|
||||
// to http sites redirects to /__/ cypress
|
||||
this._remoteStates.set(baseUrl != null ? baseUrl : '<root>')
|
||||
|
||||
return resolve([port, warning])
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
startWebsockets (automation, config, options: Record<string, unknown> = {}) {
|
||||
options.onResolveUrl = this._onResolveUrl.bind(this)
|
||||
|
||||
return super.startWebsockets(automation, config, options)
|
||||
}
|
||||
|
||||
_onResolveUrl (urlStr, headers, automationRequest, options: Record<string, any> = { headers: {} }) {
|
||||
let p
|
||||
|
||||
debug('resolving visit %o', {
|
||||
url: urlStr,
|
||||
headers,
|
||||
options,
|
||||
})
|
||||
|
||||
// always clear buffers - reduces the possibility of a random HTTP request
|
||||
// accidentally retrieving buffered content at the wrong time
|
||||
this._networkProxy?.reset()
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
// if we have an existing url resolver
|
||||
// in flight then cancel it
|
||||
if (this._urlResolver) {
|
||||
this._urlResolver.cancel()
|
||||
}
|
||||
|
||||
const request = this.request
|
||||
|
||||
let handlingLocalFile = false
|
||||
const previousRemoteState = this._remoteStates.current()
|
||||
const previousRemoteStateIsPrimary = this._remoteStates.isPrimarySuperDomainOrigin(previousRemoteState.origin)
|
||||
const primaryRemoteState = this._remoteStates.getPrimary()
|
||||
|
||||
// nuke any hashes from our url since
|
||||
// those those are client only and do
|
||||
// not apply to http requests
|
||||
urlStr = url.parse(urlStr)
|
||||
urlStr.hash = null
|
||||
urlStr = urlStr.format()
|
||||
|
||||
const originalUrl = urlStr
|
||||
|
||||
let reqStream = null
|
||||
let currentPromisePhase = null
|
||||
|
||||
const runPhase = (fn) => {
|
||||
return currentPromisePhase = fn()
|
||||
}
|
||||
|
||||
const matchesNetStubbingRoute = (requestOptions) => {
|
||||
const proxiedReq = {
|
||||
proxiedUrl: requestOptions.url,
|
||||
resourceType: 'document',
|
||||
..._.pick(requestOptions, ['headers', 'method']),
|
||||
// TODO: add `body` here once bodies can be statically matched
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const iterator = getRoutesForRequest(this.netStubbingState?.routes, proxiedReq)
|
||||
// If the iterator is exhausted (done) on the first try, then 0 matches were found
|
||||
const zeroMatches = iterator.next().done
|
||||
|
||||
return !zeroMatches
|
||||
}
|
||||
|
||||
return this._urlResolver = (p = new Bluebird<Record<string, any>>((resolve, reject, onCancel) => {
|
||||
let urlFile
|
||||
|
||||
onCancel?.(() => {
|
||||
p.currentPromisePhase = currentPromisePhase
|
||||
p.reqStream = reqStream
|
||||
|
||||
_.invoke(reqStream, 'abort')
|
||||
|
||||
return _.invoke(currentPromisePhase, 'cancel')
|
||||
})
|
||||
|
||||
const redirects: any[] = []
|
||||
let newUrl: string | null = null
|
||||
|
||||
if (!fullyQualifiedRe.test(urlStr)) {
|
||||
handlingLocalFile = true
|
||||
|
||||
options.headers['x-cypress-authorization'] = this._fileServer?.token
|
||||
|
||||
const state = this._remoteStates.set(urlStr, options)
|
||||
|
||||
// TODO: Update url.resolve signature to not use deprecated methods
|
||||
urlFile = url.resolve(state.fileServer as string, urlStr)
|
||||
urlStr = url.resolve(state.origin as string, urlStr)
|
||||
}
|
||||
|
||||
const onReqError = (err) => {
|
||||
// only restore the previous state
|
||||
// if our promise is still pending
|
||||
if (p.isPending()) {
|
||||
restorePreviousRemoteState(previousRemoteState, previousRemoteStateIsPrimary)
|
||||
}
|
||||
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
const onReqStreamReady = (str) => {
|
||||
reqStream = str
|
||||
|
||||
return str
|
||||
.on('error', onReqError)
|
||||
.on('response', (incomingRes) => {
|
||||
debug(
|
||||
'resolve:url headers received, buffering response %o',
|
||||
_.pick(incomingRes, 'headers', 'statusCode'),
|
||||
)
|
||||
|
||||
if (newUrl == null) {
|
||||
newUrl = urlStr
|
||||
}
|
||||
|
||||
return runPhase(() => {
|
||||
// get the cookies that would be sent with this request so they can be rehydrated
|
||||
return automationRequest('get:cookies', {
|
||||
domain: cors.getSuperDomain(newUrl),
|
||||
})
|
||||
.then((cookies) => {
|
||||
const statusIs2xxOrAllowedFailure = () => {
|
||||
// is our status code in the 2xx range, or have we disabled failing
|
||||
// on status code?
|
||||
return statusCode.isOk(incomingRes.statusCode) || options.failOnStatusCode === false
|
||||
}
|
||||
|
||||
const isOk = statusIs2xxOrAllowedFailure()
|
||||
const contentType = headersUtil.getContentType(incomingRes)
|
||||
|
||||
const details: Record<string, unknown> = {
|
||||
isOkStatusCode: isOk,
|
||||
contentType,
|
||||
url: newUrl,
|
||||
status: incomingRes.statusCode,
|
||||
cookies,
|
||||
statusText: statusCode.getText(incomingRes.statusCode),
|
||||
redirects,
|
||||
originalUrl,
|
||||
}
|
||||
|
||||
// does this response have this cypress header?
|
||||
const fp = incomingRes.headers['x-cypress-file-path']
|
||||
|
||||
if (fp) {
|
||||
// if so we know this is a local file request
|
||||
details.filePath = fp
|
||||
}
|
||||
|
||||
debug('setting details resolving url %o', details)
|
||||
|
||||
const concatStr = concatStream((responseBuffer) => {
|
||||
// buffer the entire response before resolving.
|
||||
// this allows us to detect & reject ETIMEDOUT errors
|
||||
// where the headers have been sent but the
|
||||
// connection hangs before receiving a body.
|
||||
|
||||
// if there is not a content-type, try to determine
|
||||
// if the response content is HTML-like
|
||||
// https://github.com/cypress-io/cypress/issues/1727
|
||||
details.isHtml = isResponseHtml(contentType, responseBuffer)
|
||||
|
||||
debug('resolve:url response ended, setting buffer %o', { newUrl, details })
|
||||
|
||||
details.totalTime = Date.now() - startTime
|
||||
|
||||
// buffer the response and set the remote state if this is a successful html response
|
||||
// TODO: think about moving this logic back into the frontend so that the driver can be in control
|
||||
// of when to buffer and set the remote state
|
||||
if (isOk && details.isHtml) {
|
||||
const urlDoesNotMatchPolicyBasedOnDomain = options.hasAlreadyVisitedUrl
|
||||
&& !cors.urlMatchesPolicyBasedOnDomain(primaryRemoteState.origin, newUrl || '', { skipDomainInjectionForDomains: this.skipDomainInjectionForDomains })
|
||||
|| options.isFromSpecBridge
|
||||
|
||||
if (!handlingLocalFile) {
|
||||
this._remoteStates.set(newUrl as string, options, !urlDoesNotMatchPolicyBasedOnDomain)
|
||||
}
|
||||
|
||||
const responseBufferStream = new stream.PassThrough({
|
||||
highWaterMark: Number.MAX_SAFE_INTEGER,
|
||||
})
|
||||
|
||||
responseBufferStream.end(responseBuffer)
|
||||
|
||||
this._networkProxy?.setHttpBuffer({
|
||||
url: newUrl,
|
||||
stream: responseBufferStream,
|
||||
details,
|
||||
originalUrl,
|
||||
response: incomingRes,
|
||||
urlDoesNotMatchPolicyBasedOnDomain,
|
||||
})
|
||||
} else {
|
||||
// TODO: move this logic to the driver too for
|
||||
// the same reasons listed above
|
||||
restorePreviousRemoteState(previousRemoteState, previousRemoteStateIsPrimary)
|
||||
}
|
||||
|
||||
details.isPrimarySuperDomainOrigin = this._remoteStates.isPrimarySuperDomainOrigin(newUrl!)
|
||||
|
||||
return resolve(details)
|
||||
})
|
||||
|
||||
return str.pipe(concatStr)
|
||||
}).catch(onReqError)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const restorePreviousRemoteState = (previousRemoteState: Cypress.RemoteState, previousRemoteStateIsPrimary: boolean) => {
|
||||
this._remoteStates.set(previousRemoteState, {}, previousRemoteStateIsPrimary)
|
||||
}
|
||||
|
||||
// if they're POSTing an object, querystringify their POST body
|
||||
if ((options.method === 'POST') && _.isObject(options.body)) {
|
||||
options.form = options.body
|
||||
delete options.body
|
||||
}
|
||||
|
||||
_.assign(options, {
|
||||
// turn off gzip since we need to eventually
|
||||
// rewrite these contents
|
||||
gzip: false,
|
||||
url: urlFile != null ? urlFile : urlStr,
|
||||
headers: _.assign({
|
||||
accept: 'text/html,*/*',
|
||||
}, options.headers),
|
||||
onBeforeReqInit: runPhase,
|
||||
followRedirect (incomingRes) {
|
||||
const status = incomingRes.statusCode
|
||||
const next = incomingRes.headers.location
|
||||
|
||||
const curr = newUrl != null ? newUrl : urlStr
|
||||
|
||||
newUrl = url.resolve(curr, next)
|
||||
|
||||
redirects.push([status, newUrl].join(': '))
|
||||
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
||||
if (matchesNetStubbingRoute(options)) {
|
||||
// TODO: this is being used to force cy.visits to be interceptable by network stubbing
|
||||
// however, network errors will be obsfucated by the proxying so this is not an ideal solution
|
||||
_.merge(options, {
|
||||
proxy: `http://127.0.0.1:${this._port()}`,
|
||||
agent: null,
|
||||
headers: {
|
||||
'x-cypress-resolving-url': '1',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
debug('sending request with options %o', options)
|
||||
|
||||
return runPhase(() => {
|
||||
// @ts-ignore
|
||||
return request.sendStream(headers, automationRequest, options)
|
||||
.then((createReqStream) => {
|
||||
const stream = createReqStream()
|
||||
|
||||
return onReqStreamReady(stream)
|
||||
}).catch(onReqError)
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
onTestFileChange (filePath) {
|
||||
return this.socket.onTestFileChange(filePath)
|
||||
}
|
||||
|
||||
_retryBaseUrlCheck (baseUrl, onWarning) {
|
||||
return ensureUrl.retryIsListening(baseUrl, {
|
||||
retryIntervals: [3000, 3000, 4000],
|
||||
onRetry ({ attempt, delay, remaining }) {
|
||||
const warning = errors.get('CANNOT_CONNECT_BASE_URL_RETRYING', {
|
||||
remaining,
|
||||
attempt,
|
||||
delay,
|
||||
baseUrl,
|
||||
})
|
||||
|
||||
return onWarning(warning)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ const cache = require(`../../lib/cache`)
|
||||
const errors = require(`../../lib/errors`)
|
||||
const cypress = require(`../../lib/cypress`)
|
||||
const ProjectBase = require(`../../lib/project-base`).ProjectBase
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
const { ServerBase } = require(`../../lib/server-base`)
|
||||
const Reporter = require(`../../lib/reporter`)
|
||||
const browsers = require(`../../lib/browsers`)
|
||||
const videoCapture = require(`../../lib/video_capture`)
|
||||
@@ -174,7 +174,7 @@ describe('lib/cypress', () => {
|
||||
sinon.stub(extension, 'setHostAndPath').resolves()
|
||||
sinon.stub(detect, 'detect').resolves(TYPICAL_BROWSERS)
|
||||
sinon.stub(process, 'exit')
|
||||
sinon.stub(ServerE2E.prototype, 'reset')
|
||||
sinon.stub(ServerBase.prototype, 'reset')
|
||||
sinon.stub(errors, 'warning')
|
||||
.callThrough()
|
||||
.withArgs('INVOKED_BINARY_OUTSIDE_NPM_MODULE')
|
||||
@@ -1102,7 +1102,7 @@ describe('lib/cypress', () => {
|
||||
|
||||
it('can change the default port to 5544', function () {
|
||||
const listen = sinon.spy(http.Server.prototype, 'listen')
|
||||
const open = sinon.spy(ServerE2E.prototype, 'open')
|
||||
const open = sinon.spy(ServerBase.prototype, 'open')
|
||||
|
||||
return cypress.start([`--run-project=${this.todosPath}`, '--port=5544'])
|
||||
.then(() => {
|
||||
@@ -1777,7 +1777,7 @@ describe('lib/cypress', () => {
|
||||
|
||||
sinon.stub(electron.app, 'on').withArgs('ready').yieldsAsync()
|
||||
sinon.stub(Windows, 'open').resolves(this.win)
|
||||
sinon.stub(ServerE2E.prototype, 'startWebsockets')
|
||||
sinon.stub(ServerBase.prototype, 'startWebsockets')
|
||||
sinon.stub(electron.ipcMain, 'on')
|
||||
})
|
||||
|
||||
@@ -1798,7 +1798,7 @@ describe('lib/cypress', () => {
|
||||
|
||||
// TODO: fix failing test https://github.com/cypress-io/cypress/issues/23149
|
||||
it.skip('passes filtered options to Project#open and sets cli config', async function () {
|
||||
const open = sinon.stub(ServerE2E.prototype, 'open').resolves([])
|
||||
const open = sinon.stub(ServerBase.prototype, 'open').resolves([])
|
||||
|
||||
sinon.stub(interactiveMode, 'ready')
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const SseStream = require('ssestream')
|
||||
const EventSource = require('eventsource')
|
||||
const { setupFullConfigWithDefaults } = require('@packages/config')
|
||||
const config = require(`../../lib/config`)
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
const { ServerBase } = require(`../../lib/server-base`)
|
||||
const pluginsModule = require(`../../lib/plugins`)
|
||||
const preprocessor = require(`../../lib/plugins/preprocessor`)
|
||||
const resolve = require(`../../lib/util/resolve`)
|
||||
@@ -87,7 +87,7 @@ describe('Routes', () => {
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
||||
|
||||
sinon.stub(CacheBuster, 'get').returns('-123')
|
||||
sinon.stub(ServerE2E.prototype, 'reset')
|
||||
sinon.stub(ServerBase.prototype, 'reset')
|
||||
sinon.stub(pluginsModule, 'has').returns(false)
|
||||
|
||||
nock.enableNetConnect()
|
||||
@@ -161,7 +161,7 @@ describe('Routes', () => {
|
||||
httpsServer.start(8443),
|
||||
|
||||
// and open our cypress server
|
||||
(this.server = new ServerE2E()),
|
||||
(this.server = new ServerBase()),
|
||||
|
||||
this.server.open(cfg, {
|
||||
SocketCtor: SocketE2E,
|
||||
|
||||
@@ -8,7 +8,7 @@ const evilDns = require('evil-dns')
|
||||
const { setupFullConfigWithDefaults } = require('@packages/config')
|
||||
const httpsServer = require(`@packages/https-proxy/test/helpers/https_server`)
|
||||
const config = require(`../../lib/config`)
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
const { ServerBase } = require(`../../lib/server-base`)
|
||||
const { SocketE2E } = require(`../../lib/socket-e2e`)
|
||||
const Fixtures = require('@tooling/system-tests')
|
||||
const { createRoutes } = require(`../../lib/routes`)
|
||||
@@ -26,7 +26,7 @@ describe('Server', () => {
|
||||
require('mocha-banner').register()
|
||||
|
||||
beforeEach(() => {
|
||||
return sinon.stub(ServerE2E.prototype, 'reset')
|
||||
return sinon.stub(ServerBase.prototype, 'reset')
|
||||
})
|
||||
|
||||
context('resolving url', () => {
|
||||
@@ -83,7 +83,7 @@ describe('Server', () => {
|
||||
httpsServer.start(8443),
|
||||
|
||||
// and open our cypress server
|
||||
(this.server = new ServerE2E()),
|
||||
(this.server = new ServerBase()),
|
||||
|
||||
this.server.open(cfg, {
|
||||
SocketCtor: SocketE2E,
|
||||
|
||||
@@ -8,7 +8,7 @@ const Promise = require('bluebird')
|
||||
const socketIo = require(`@packages/socket/lib/browser`)
|
||||
const httpsServer = require(`@packages/https-proxy/test/helpers/https_server`)
|
||||
const config = require(`../../lib/config`)
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
const { ServerBase } = require(`../../lib/server-base`)
|
||||
const { SocketE2E } = require(`../../lib/socket-e2e`)
|
||||
const { Automation } = require(`../../lib/automation`)
|
||||
const Fixtures = require('@tooling/system-tests')
|
||||
@@ -38,7 +38,7 @@ describe('Web Sockets', () => {
|
||||
this.cfg = cfg
|
||||
this.ws = new ws.Server({ port: wsPort })
|
||||
|
||||
this.server = new ServerE2E()
|
||||
this.server = new ServerBase()
|
||||
|
||||
return this.server.open(this.cfg, {
|
||||
SocketCtor: SocketE2E,
|
||||
|
||||
@@ -22,7 +22,7 @@ process.env.CYPRESS_INTERNAL_ENV = 'development'
|
||||
|
||||
const CA = require('@packages/https-proxy').CA
|
||||
const { setupFullConfigWithDefaults } = require('@packages/config')
|
||||
const { ServerE2E } = require('../../lib/server-e2e')
|
||||
const { ServerBase } = require('../../lib/server-base')
|
||||
const { SocketE2E } = require('../../lib/socket-e2e')
|
||||
const { _getArgs } = require('../../lib/browsers/chrome')
|
||||
|
||||
@@ -361,7 +361,7 @@ describe('Proxy Performance', function () {
|
||||
// turn off morgan
|
||||
config.morgan = false
|
||||
|
||||
cyServer = new ServerE2E()
|
||||
cyServer = new ServerBase()
|
||||
|
||||
return cyServer.open(config, {
|
||||
SocketCtor: SocketE2E,
|
||||
|
||||
@@ -6,7 +6,7 @@ const pkg = require('@packages/root')
|
||||
const Fixtures = require('@tooling/system-tests')
|
||||
const { sinon } = require('../spec_helper')
|
||||
const config = require(`../../lib/config`)
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
const { ServerBase } = require(`../../lib/server-base`)
|
||||
const { ProjectBase } = require(`../../lib/project-base`)
|
||||
const { Automation } = require(`../../lib/automation`)
|
||||
const savedState = require(`../../lib/saved_state`)
|
||||
@@ -224,8 +224,8 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
beforeEach(function () {
|
||||
sinon.stub(this.project, 'startWebsockets')
|
||||
sinon.stub(this.project, 'getConfig').returns(this.config)
|
||||
sinon.stub(ServerE2E.prototype, 'open').resolves([])
|
||||
sinon.stub(ServerE2E.prototype, 'reset')
|
||||
sinon.stub(ServerBase.prototype, 'open').resolves([])
|
||||
sinon.stub(ServerBase.prototype, 'reset')
|
||||
})
|
||||
|
||||
it('calls #startWebsockets with options + config', function () {
|
||||
|
||||
@@ -6,7 +6,7 @@ const express = require('express')
|
||||
const Promise = require('bluebird')
|
||||
const { connect } = require('@packages/network')
|
||||
const { setupFullConfigWithDefaults } = require('@packages/config')
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
const { ServerBase } = require(`../../lib/server-base`)
|
||||
const { SocketE2E } = require(`../../lib/socket-e2e`)
|
||||
const fileServer = require(`../../lib/file_server`)
|
||||
const ensureUrl = require(`../../lib/util/ensure-url`)
|
||||
@@ -20,7 +20,7 @@ mockery.registerMock('morgan', () => {
|
||||
|
||||
describe('lib/server', () => {
|
||||
beforeEach(function () {
|
||||
this.server = new ServerE2E()
|
||||
this.server = new ServerBase()
|
||||
|
||||
return setupFullConfigWithDefaults({ projectRoot: '/foo/bar/', config: { supportFile: false } }, getCtx().file.getFilesByGlob)
|
||||
.then((cfg) => {
|
||||
@@ -54,7 +54,7 @@ describe.skip('lib/server', () => {
|
||||
return setupFullConfigWithDefaults({ projectRoot: '/foo/bar/' }, getCtx().file.getFilesByGlob)
|
||||
.then((cfg) => {
|
||||
this.config = cfg
|
||||
this.server = new ServerE2E()
|
||||
this.server = new ServerBase()
|
||||
|
||||
this.oldFileServer = this.server._fileServer
|
||||
this.server._fileServer = this.fileServer
|
||||
|
||||
@@ -9,7 +9,7 @@ const Fixtures = require('@tooling/system-tests')
|
||||
|
||||
const errors = require('../../lib/errors')
|
||||
const { SocketE2E } = require('../../lib/socket-e2e')
|
||||
const { ServerE2E } = require('../../lib/server-e2e')
|
||||
const { ServerBase } = require('../../lib/server-base')
|
||||
const { Automation } = require('../../lib/automation')
|
||||
const preprocessor = require('../../lib/plugins/preprocessor')
|
||||
const { fs } = require('../../lib/util/fs')
|
||||
@@ -37,7 +37,7 @@ describe('lib/socket', () => {
|
||||
|
||||
this.todosPath = Fixtures.projectPath('todos')
|
||||
|
||||
this.server = new ServerE2E()
|
||||
this.server = new ServerBase()
|
||||
|
||||
await ctx.actions.project.setCurrentProjectAndTestingTypeForTestSetup(this.todosPath)
|
||||
|
||||
|
||||
@@ -4073,13 +4073,11 @@
|
||||
"./packages/server/lib/project_utils.ts",
|
||||
"./packages/server/lib/remote_states.ts",
|
||||
"./packages/server/lib/request.js",
|
||||
"./packages/server/lib/routes-ct.ts",
|
||||
"./packages/server/lib/routes-e2e.ts",
|
||||
"./packages/server/lib/routes.ts",
|
||||
"./packages/server/lib/saved_state.ts",
|
||||
"./packages/server/lib/server-base.ts",
|
||||
"./packages/server/lib/server-ct.ts",
|
||||
"./packages/server/lib/server-e2e.ts",
|
||||
"./packages/server/lib/server-base.ts",
|
||||
"./packages/server/lib/session.ts",
|
||||
"./packages/server/lib/socket-base.ts",
|
||||
"./packages/server/lib/socket-e2e.ts",
|
||||
|
||||
@@ -4072,13 +4072,11 @@
|
||||
"./packages/server/lib/project_utils.ts",
|
||||
"./packages/server/lib/remote_states.ts",
|
||||
"./packages/server/lib/request.js",
|
||||
"./packages/server/lib/routes-ct.ts",
|
||||
"./packages/server/lib/routes-e2e.ts",
|
||||
"./packages/server/lib/routes.ts",
|
||||
"./packages/server/lib/saved_state.ts",
|
||||
"./packages/server/lib/server-base.ts",
|
||||
"./packages/server/lib/server-ct.ts",
|
||||
"./packages/server/lib/server-e2e.ts",
|
||||
"./packages/server/lib/server-base.ts",
|
||||
"./packages/server/lib/session.ts",
|
||||
"./packages/server/lib/socket-base.ts",
|
||||
"./packages/server/lib/socket-e2e.ts",
|
||||
|
||||
@@ -4070,13 +4070,11 @@
|
||||
"./packages/server/lib/project_utils.ts",
|
||||
"./packages/server/lib/remote_states.ts",
|
||||
"./packages/server/lib/request.js",
|
||||
"./packages/server/lib/routes-ct.ts",
|
||||
"./packages/server/lib/routes-e2e.ts",
|
||||
"./packages/server/lib/routes.ts",
|
||||
"./packages/server/lib/saved_state.ts",
|
||||
"./packages/server/lib/server-base.ts",
|
||||
"./packages/server/lib/server-ct.ts",
|
||||
"./packages/server/lib/server-e2e.ts",
|
||||
"./packages/server/lib/server-base.ts",
|
||||
"./packages/server/lib/session.ts",
|
||||
"./packages/server/lib/socket-base.ts",
|
||||
"./packages/server/lib/socket-e2e.ts",
|
||||
|
||||
Reference in New Issue
Block a user