mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-06 23:10:22 -05:00
feat: improved DX and support for running component and e2e tests w/ gulp (#18135)
Co-authored-by: Tim Griesser <tgriesser10@gmail.com>
This commit is contained in:
+53
-5
@@ -385,18 +385,26 @@ commands:
|
||||
path: /tmp/artifacts
|
||||
- store-npm-logs
|
||||
|
||||
run-launchpad-integration-tests:
|
||||
run-new-ui-tests:
|
||||
parameters:
|
||||
package:
|
||||
description: package to target
|
||||
type: enum
|
||||
enum: ['launchpad', 'app']
|
||||
browser:
|
||||
description: browser shortname to target
|
||||
type: string
|
||||
type:
|
||||
description: ct or e2e
|
||||
type: enum
|
||||
enum: ['ct', 'e2e']
|
||||
steps:
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
command: |
|
||||
CYPRESS_KONFIG_ENV=production \
|
||||
CYPRESS_RECORD_KEY=$TEST_LAUNCHPAD_RECORD_KEY \
|
||||
yarn workspace @packages/launchpad cypress:run --browser <<parameters.browser>> --record --parallel
|
||||
yarn workspace @packages/<<parameters.package>> cypress:run:<<parameters.type>> --browser <<parameters.browser>> --record --parallel --group <<parameters.package>>-<<parameters.type>>
|
||||
- store_test_results:
|
||||
path: /tmp/cypress
|
||||
- store_artifacts:
|
||||
@@ -1165,12 +1173,41 @@ jobs:
|
||||
browser: chrome
|
||||
percy: true
|
||||
|
||||
run-launchpad-integration-tests-chrome:
|
||||
run-launchpad-component-tests-chrome:
|
||||
<<: *defaults
|
||||
parallelism: 3
|
||||
steps:
|
||||
- run-launchpad-integration-tests:
|
||||
- run-new-ui-tests:
|
||||
browser: chrome
|
||||
package: launchpad
|
||||
type: ct
|
||||
|
||||
run-launchpad-integration-tests-chrome:
|
||||
<<: *defaults
|
||||
parallelism: 1
|
||||
steps:
|
||||
- run-new-ui-tests:
|
||||
browser: chrome
|
||||
package: launchpad
|
||||
type: e2e
|
||||
|
||||
# run-app-component-tests-chrome:
|
||||
# <<: *defaults
|
||||
# parallelism: 1
|
||||
# steps:
|
||||
# - run-new-ui-tests:
|
||||
# browser: chrome
|
||||
# package: app
|
||||
# type: ct
|
||||
|
||||
run-app-integration-tests-chrome:
|
||||
<<: *defaults
|
||||
parallelism: 1
|
||||
steps:
|
||||
- run-new-ui-tests:
|
||||
browser: chrome
|
||||
package: app
|
||||
type: e2e
|
||||
|
||||
driver-integration-tests-chrome:
|
||||
<<: *defaults
|
||||
@@ -2068,7 +2105,18 @@ linux-workflow: &linux-workflow
|
||||
context: test-runner:launchpad-tests
|
||||
requires:
|
||||
- build
|
||||
|
||||
- run-launchpad-component-tests-chrome:
|
||||
context: test-runner:launchpad-tests
|
||||
requires:
|
||||
- build
|
||||
- run-app-integration-tests-chrome:
|
||||
context: test-runner:launchpad-tests
|
||||
requires:
|
||||
- build
|
||||
# - run-app-component-tests-chrome:
|
||||
# context: test-runner:launchpad-tests
|
||||
# requires:
|
||||
# - build
|
||||
- desktop-gui-integration-tests-2x:
|
||||
requires:
|
||||
- build
|
||||
|
||||
+11
-21
@@ -153,29 +153,19 @@ module.exports = {
|
||||
args = [...electronArgs, '--', ...args]
|
||||
}
|
||||
|
||||
debug('spawn args %o %o', args, _.omit(stdioOptions, 'env'))
|
||||
let child
|
||||
|
||||
if (process.env.CYPRESS_INTERNAL_DEV_WATCH) {
|
||||
if (process.env.CYPRESS_INTERNAL_DEV_DEBUG) {
|
||||
stdioOptions.execArgv = [process.env.CYPRESS_INTERNAL_DEV_DEBUG]
|
||||
}
|
||||
|
||||
debug('spawning Cypress as fork: %s', startScriptPath)
|
||||
|
||||
child = cp.fork(startScriptPath, args, stdioOptions)
|
||||
process.on('message', (msg) => {
|
||||
child.send(msg)
|
||||
})
|
||||
} else {
|
||||
debug('spawning Cypress with executable: %s', executable)
|
||||
if (startScriptPath) {
|
||||
args.unshift(startScriptPath)
|
||||
}
|
||||
|
||||
child = cp.spawn(executable, args, stdioOptions)
|
||||
if (startScriptPath) {
|
||||
args.unshift(startScriptPath)
|
||||
}
|
||||
|
||||
if (process.env.CYPRESS_INTERNAL_DEV_DEBUG) {
|
||||
args.unshift(process.env.CYPRESS_INTERNAL_DEV_DEBUG)
|
||||
}
|
||||
|
||||
debug('spawn args %o %o', args, _.omit(stdioOptions, 'env'))
|
||||
debug('spawning Cypress with executable: %s', executable)
|
||||
|
||||
const child = cp.spawn(executable, args, stdioOptions)
|
||||
|
||||
function resolveOn (event) {
|
||||
return function (code, signal) {
|
||||
debug('child event fired %o', { event, code, signal })
|
||||
|
||||
@@ -58,6 +58,12 @@ module.exports = {
|
||||
},
|
||||
|
||||
isNeeded () {
|
||||
// Used when we're using Cypress to test Cypress - the headless Cypress
|
||||
// doesn't need Xvfb
|
||||
if (process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (process.env.ELECTRON_RUN_AS_NODE) {
|
||||
debug('Environment variable ELECTRON_RUN_AS_NODE detected, xvfb is not needed')
|
||||
|
||||
|
||||
@@ -12,5 +12,5 @@ export async function startDevServer (startDevServerArgs: StartDevServerOptions)
|
||||
|
||||
debug('Component testing vite server started on port', port)
|
||||
|
||||
return { port, close: app.httpServer!.close }
|
||||
return { port, close: viteDevServer.close }
|
||||
}
|
||||
|
||||
@@ -207,6 +207,7 @@
|
||||
"semantic-release": "17.2.3",
|
||||
"semantic-release-monorepo": "7.0.3",
|
||||
"semver": "7.3.2",
|
||||
"serve": "12.0.1",
|
||||
"shelljs": "0.8.3",
|
||||
"shx": "0.3.3",
|
||||
"sinon": "7.3.2",
|
||||
@@ -218,6 +219,7 @@
|
||||
"terminal-banner": "1.1.0",
|
||||
"through": "2.3.8",
|
||||
"through2": "^4.0.2",
|
||||
"tree-kill": "1.2.2",
|
||||
"ts-node": "8.3.0",
|
||||
"typescript": "^4.2.3",
|
||||
"yarn-deduplicate": "3.1.0"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"projectId": "ypt4pf",
|
||||
"baseUrl": "http://localhost:5555",
|
||||
"projectId": "sehy69",
|
||||
"baseUrl": "http://localhost:5556",
|
||||
"viewportWidth": 800,
|
||||
"viewportHeight": 850,
|
||||
"fixturesFolder": false,
|
||||
"video": false,
|
||||
"retries": {
|
||||
"runMode": 2,
|
||||
"openMode": 0
|
||||
@@ -17,5 +16,8 @@
|
||||
},
|
||||
"component": {
|
||||
"componentFolder": "src"
|
||||
},
|
||||
"e2e": {
|
||||
"supportFile": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
describe('Launchpad', () => {
|
||||
it('resolves the home page', () => {
|
||||
cy.visit('http://localhost:5556')
|
||||
})
|
||||
})
|
||||
@@ -9,8 +9,8 @@
|
||||
"clean-deps": "rimraf node_modules",
|
||||
"test": "echo 'ok'",
|
||||
"cypress:launch": "cross-env TZ=America/New_York node ../../scripts/cypress open --project ${PWD}",
|
||||
"cypress:open": "cross-env TZ=America/New_York node ../../scripts/cypress open-ct --project ${PWD}",
|
||||
"cypress:run": "cross-env TZ=America/New_York node ../../scripts/cypress run-ct --project ${PWD}",
|
||||
"cypress:open": "yarn gulp cyOpenAppE2E",
|
||||
"cypress:run:e2e": "yarn gulp cyRunAppE2E",
|
||||
"dev": "gulp dev --project ${PWD}",
|
||||
"start": "echo \"run 'yarn dev' from the root\" && exit 1",
|
||||
"watch": "echo \"run 'yarn dev' from the root\" && exit 1",
|
||||
|
||||
@@ -35,7 +35,7 @@ export class ProjectDataSource {
|
||||
}
|
||||
|
||||
// unexpected error
|
||||
throw Error(e)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,16 +156,6 @@ module.exports = {
|
||||
return process.exit(code)
|
||||
})
|
||||
|
||||
if (process.env.CYPRESS_INTERNAL_DEV_WATCH) {
|
||||
spawned.on('exit', () => {
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
process.on('exit', () => {
|
||||
spawned.kill(9)
|
||||
})
|
||||
}
|
||||
|
||||
return spawned
|
||||
}).catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"baseUrl": "http://localhost:5555",
|
||||
"viewportWidth": 800,
|
||||
"viewportHeight": 850,
|
||||
"video": false,
|
||||
"retries": {
|
||||
"runMode": 2,
|
||||
"openMode": 0
|
||||
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
// Unsure why the ImportMeta type isn't picking this up.
|
||||
interface ImportMetaEnv {
|
||||
VITE_CYPRESS_INTERNAL_GQL_PORT: string
|
||||
}
|
||||
@@ -55,5 +55,6 @@
|
||||
"windicss": "3.1.8",
|
||||
"windicss-analysis": "^0.3.4",
|
||||
"wonka": "^4.0.15"
|
||||
}
|
||||
},
|
||||
"module": "es2020"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@urql/core'
|
||||
import { devtoolsExchange } from '@urql/devtools'
|
||||
import { cacheExchange as graphcacheExchange } from '@urql/exchange-graphcache'
|
||||
import { GRAPHQL_URL } from '../utils/env'
|
||||
|
||||
export function makeCacheExchange () {
|
||||
return graphcacheExchange({
|
||||
@@ -38,7 +39,7 @@ export function makeUrqlClient (): Client {
|
||||
}
|
||||
|
||||
return createClient({
|
||||
url: 'http://localhost:52159/graphql',
|
||||
url: GRAPHQL_URL,
|
||||
requestPolicy: 'cache-and-network',
|
||||
exchanges,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// This code is meant to be executed within Vite
|
||||
// which supports environment variables being injected into the client at build time
|
||||
|
||||
export const GRAPHQL_PORT = import.meta.env.VITE_CYPRESS_INTERNAL_GQL_PORT || `${52200}`
|
||||
|
||||
export const GRAPHQL_URL = `http://localhost:${GRAPHQL_PORT}/graphql`
|
||||
@@ -13,7 +13,7 @@ With the goal of type safety, several tools and abstractions are used. The techn
|
||||
|
||||
You will generally develop this in parallel with a front-end, in this case `@packages/launchpad`. Run `yarn dev` in `@packages/launchpad` and it will start up the GraphQL server. This also re-generates the `graphql.schema` file based on the declarations inside of [entities](https://github.com/cypress-io/cypress/blob/develop/packages/graphql/src/entities).
|
||||
|
||||
Visit `http://localhost:52159/graphql` for the GraphiQL interface.
|
||||
Visit `http://localhost:52200/graphql` for the GraphiQL interface.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"dedent": "^0.7.0",
|
||||
"express": "4.17.1",
|
||||
"express-graphql": "^0.12.0",
|
||||
"getenv": "^1.0.0",
|
||||
"getenv": "1.0.0",
|
||||
"graphql": "^15.5.1",
|
||||
"graphql-scalars": "^1.10.0",
|
||||
"nexus": "^1.2.0-next.15"
|
||||
|
||||
@@ -3,3 +3,5 @@ export { graphqlSchema } from './schema'
|
||||
export { execute, parse, print } from 'graphql'
|
||||
|
||||
export { remoteSchemaWrapped } from './stitching/remoteSchemaWrapped'
|
||||
|
||||
//
|
||||
|
||||
@@ -4,10 +4,12 @@ import Debug from 'debug'
|
||||
import type { Server } from 'http'
|
||||
import type { AddressInfo } from 'net'
|
||||
import cors from 'cors'
|
||||
import getenv from 'getenv'
|
||||
import { graphqlSchema } from './schema'
|
||||
import type { DataContext } from '@packages/data-context'
|
||||
|
||||
const debug = Debug('cypress:server:graphql')
|
||||
const GRAPHQL_PORT = getenv.int('CYPRESS_INTERNAL_GQL_PORT', 52200)
|
||||
|
||||
let app: ReturnType<typeof express>
|
||||
let server: Server
|
||||
@@ -40,15 +42,13 @@ export function setDataContext (ctx: DataContext) {
|
||||
return ctx
|
||||
}
|
||||
|
||||
export function startGraphQLServer ({ port }: { port: number } = { port: 52159 }): Promise<{
|
||||
export function startGraphQLServer ({ port }: { port: number } = { port: GRAPHQL_PORT }): Promise<{
|
||||
server: Server
|
||||
app: Express.Application
|
||||
endpoint: string
|
||||
}> {
|
||||
app = express()
|
||||
|
||||
app.use(cors())
|
||||
|
||||
app.use('/graphql', graphqlHTTP((req) => {
|
||||
if (!dataContext) {
|
||||
throw new Error(`setDataContext has not been called`)
|
||||
|
||||
@@ -6,9 +6,10 @@ import getenv from 'getenv'
|
||||
import type { DataContext } from '@packages/data-context'
|
||||
|
||||
const cloudEnv = getenv('CYPRESS_INTERNAL_CLOUD_ENV', process.env.CYPRESS_INTERNAL_ENV || 'development') as keyof typeof REMOTE_SCHEMA_URLS
|
||||
|
||||
const REMOTE_SCHEMA_URLS = {
|
||||
development: 'http://localhost:3000',
|
||||
staging: 'https://dashboard-staging.cypress.io',
|
||||
development: 'http://localhost:3000',
|
||||
production: 'https://dashboard.cypress.io',
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ For the best development experience, you will want to use VS Code with the [Vola
|
||||
yarn dev
|
||||
```
|
||||
|
||||
This starts Vite in watch mode. It also starts the GraphQL Server. You can access it on `http://localhost:52159/graphql`.
|
||||
This starts Vite in watch mode. It also starts the GraphQL Server. You can access it on `http://localhost:52200/graphql`.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"baseUrl": "http://localhost:5555",
|
||||
"viewportWidth": 800,
|
||||
"viewportHeight": 850,
|
||||
"video": false,
|
||||
"retries": {
|
||||
"runMode": 2,
|
||||
"openMode": 0
|
||||
@@ -16,7 +15,9 @@
|
||||
},
|
||||
"componentFolder": "src",
|
||||
"component": {
|
||||
"testFiles": "**/*.spec.{js,ts,tsx,jsx}"
|
||||
"testFiles": "**/*.spec.{js,ts,tsx,jsx}",
|
||||
"supportFile": "cypress/component/support/index.ts",
|
||||
"pluginsFile": "cypress/component/plugins/index.js"
|
||||
},
|
||||
"e2e": {
|
||||
"supportFile": false
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @type {import('@cypress/vite-dev-server')}
|
||||
*/
|
||||
const { startDevServer } = require('@cypress/vite-dev-server')
|
||||
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
|
||||
if (config.testingType === 'component') {
|
||||
on('dev-server:start', async (options) => {
|
||||
return startDevServer({
|
||||
options,
|
||||
viteConfig: {
|
||||
// TODO(tim): Figure out why this isn't being picked up
|
||||
optimizeDeps: {
|
||||
include: ['@headlessui/vue', 'vue-prism-component'],
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return config // IMPORTANT to return a config
|
||||
}
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
// Import commands.js using ES2015 syntax:
|
||||
|
||||
import 'virtual:windi.css'
|
||||
import '../../src/main.scss'
|
||||
import '../../../src/main.scss'
|
||||
import '@iconify/iconify'
|
||||
import '@purge-icons/generated'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
describe('basic', () => {
|
||||
it('passes', () => {
|
||||
expect(1).to.eq(1)
|
||||
describe('Launchpad', () => {
|
||||
it('resolves the home page', () => {
|
||||
cy.visit('http://localhost:5555')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
/**
|
||||
* @type {import('@cypress/vite-dev-server')}
|
||||
*/
|
||||
const { startDevServer } = require('@cypress/vite-dev-server')
|
||||
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
@@ -13,6 +8,7 @@ const { startDevServer } = require('@cypress/vite-dev-server')
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
@@ -23,20 +19,4 @@ const { startDevServer } = require('@cypress/vite-dev-server')
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
|
||||
if (config.testingType === 'component') {
|
||||
on('dev-server:start', async (options) => {
|
||||
return startDevServer({
|
||||
options,
|
||||
viteConfig: {
|
||||
// TODO(tim): Figure out why this isn't being picked up
|
||||
optimizeDeps: {
|
||||
include: ['@headlessui/vue', 'vue-prism-component'],
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return config // IMPORTANT to return a config
|
||||
}
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
"scripts": {
|
||||
"check-ts": "vue-tsc --noEmit",
|
||||
"build-prod": "cross-env NODE_ENV=production vite build",
|
||||
"clean": "rimraf dist && rimraf ./node_modules/.vite && echo 'cleaned'",
|
||||
"clean": "rimraf dist && rimraf ./node_modules/.vite && rimraf dist-e2e && echo 'cleaned'",
|
||||
"clean-deps": "rimraf node_modules",
|
||||
"test": "yarn cypress:run && yarn types",
|
||||
"test": "yarn cypress:run:ct && yarn types",
|
||||
"windi": "yarn windicss-analysis",
|
||||
"cypress:launch": "cross-env TZ=America/New_York node ../../scripts/cypress open --project ${PWD}",
|
||||
"cypress:open": "cross-env TZ=America/New_York node ../../scripts/cypress open-ct --project ${PWD}",
|
||||
"cypress:run": "cross-env TZ=America/New_York node ../../scripts/cypress run-ct --project ${PWD}",
|
||||
"dev": "gulp dev --project ${PWD}",
|
||||
"cypress:open": "yarn gulp cyOpenLaunchpadE2E",
|
||||
"cypress:run:ct": "cross-env TZ=America/New_York node ../../scripts/cypress run-ct --project ${PWD}",
|
||||
"cypress:run:e2e": "yarn gulp cyRunLaunchpadE2E",
|
||||
"dev": "yarn gulp dev --project ${PWD}",
|
||||
"start": "echo 'run yarn dev from the root' && exit 1",
|
||||
"watch": "echo 'run yarn dev from the root' && exit 1"
|
||||
},
|
||||
|
||||
@@ -2,12 +2,18 @@ import path from 'path'
|
||||
|
||||
let fs: typeof import('fs-extra')
|
||||
|
||||
export type RunnerPkg = 'runner' | 'runner-ct'
|
||||
export type RunnerPkg = 'app' | 'runner' | 'runner-ct'
|
||||
|
||||
type FoldersWithDist = 'static' | 'driver' | RunnerPkg
|
||||
|
||||
export const getPathToDist = (folder: FoldersWithDist, ...args: string[]) => {
|
||||
return path.join(...[__dirname, '..', '..', folder, 'dist', ...args])
|
||||
let distDir = 'dist'
|
||||
|
||||
if (process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
|
||||
distDir = 'dist-e2e'
|
||||
}
|
||||
|
||||
return path.join(...[__dirname, '..', '..', folder, distDir, ...args])
|
||||
}
|
||||
|
||||
export const getRunnerInjectionContents = () => {
|
||||
@@ -21,8 +27,17 @@ export const getPathToIndex = (pkg: RunnerPkg) => {
|
||||
}
|
||||
|
||||
export const getPathToDesktopIndex = (pkg: 'desktop-gui' | 'launchpad') => {
|
||||
// TODO: check if there's a better approach to fix here
|
||||
if (pkg === 'launchpad' && !process.env.CI) return `http://localhost:3001`
|
||||
let distDir = 'dist'
|
||||
|
||||
return `file://${path.join(__dirname, '..', '..', pkg, 'dist', 'index.html')}`
|
||||
// For now, if we see that there's a CYPRESS_INTERNAL_VITE_LAUNCHPAD_PORT
|
||||
// we assume we're running Cypress targeting that (dev server)
|
||||
if (pkg === 'launchpad' && process.env.CYPRESS_INTERNAL_VITE_LAUNCHPAD_PORT) {
|
||||
return `http://localhost:${process.env.CYPRESS_INTERNAL_VITE_LAUNCHPAD_PORT}`
|
||||
}
|
||||
|
||||
if (process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
|
||||
distDir = 'dist-e2e'
|
||||
}
|
||||
|
||||
return `file://${path.join(__dirname, '..', '..', pkg, distDir, 'index.html')}`
|
||||
}
|
||||
|
||||
@@ -158,6 +158,14 @@ export function create (projectRoot, _options: WindowOptions = {}, newBrowserWin
|
||||
options.webPreferences.partition = options.partition
|
||||
}
|
||||
|
||||
// When we're E2E testing the launchpad or app, we want to stand up a real Cy server.
|
||||
// It's best to do this without rendering the launchpad, so we won't visually render the electron window.
|
||||
// TODO(jess): Is it better to stub the electron window? The server is pretty coupled to it.
|
||||
if (process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
|
||||
options.frame = false
|
||||
options.show = false
|
||||
}
|
||||
|
||||
const win = newBrowserWindow(options)
|
||||
|
||||
win.on('blur', function (...args) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// process.title = 'Cypress: Plugin Manager'
|
||||
|
||||
require('graceful-fs').gracefulify(require('fs'))
|
||||
|
||||
require('../../util/suppress_warnings').suppress()
|
||||
|
||||
@@ -26,10 +26,24 @@ export const createRoutesCT = ({
|
||||
}: InitializeRoutes) => {
|
||||
const routesCt = Router()
|
||||
|
||||
// If development
|
||||
const myProxy = httpProxy.createProxyServer({
|
||||
target: 'http://localhost:3333/',
|
||||
})
|
||||
if (process.env.CYPRESS_INTERNAL_VITE_APP_PORT) {
|
||||
const myProxy = httpProxy.createProxyServer({
|
||||
target: `http://localhost:${process.env.CYPRESS_INTERNAL_VITE_APP_PORT}/`,
|
||||
})
|
||||
|
||||
// TODO: can namespace this onto a "unified" route like __app-unified__
|
||||
// make sure to update the generated routes inside of vite.config.ts
|
||||
routesCt.get('/__vite__/*', (req, res) => {
|
||||
myProxy.web(req, res, {}, (e) => {
|
||||
})
|
||||
})
|
||||
} else {
|
||||
routesCt.get('/__vite__/*', (req, res) => {
|
||||
const pathToFile = getPathToDist('app', req.params[0])
|
||||
|
||||
return send(req, pathToFile).pipe(res)
|
||||
})
|
||||
}
|
||||
|
||||
// TODO If prod, serve the build app files from app/dist
|
||||
|
||||
@@ -43,13 +57,6 @@ export const createRoutesCT = ({
|
||||
res.json(options)
|
||||
})
|
||||
|
||||
// TODO: can namespace this onto a "unified" route like __app-unified__
|
||||
// make sure to update the generated routes inside of vite.config.ts
|
||||
routesCt.get('/__vite__/*', (req, res) => {
|
||||
myProxy.web(req, res, {}, (e) => {
|
||||
})
|
||||
})
|
||||
|
||||
routesCt.get('/__cypress/static/*', (req, res) => {
|
||||
const pathToFile = getPathToDist('static', req.params[0])
|
||||
|
||||
|
||||
@@ -92,7 +92,11 @@ module.exports = {
|
||||
'expected CYPRESS_INTERNAL_ENV, found', env.CYPRESS_INTERNAL_ENV)
|
||||
|
||||
// allow overriding the app_data folder
|
||||
const folder = env.CYPRESS_KONFIG_ENV || env.CYPRESS_INTERNAL_ENV
|
||||
let folder = env.CYPRESS_KONFIG_ENV || env.CYPRESS_INTERNAL_ENV
|
||||
|
||||
if (env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
|
||||
folder = `${folder}-e2e-test`
|
||||
}
|
||||
|
||||
const p = path.join(ELECTRON_APP_DATA_PATH, 'cy', folder, ...paths)
|
||||
|
||||
|
||||
@@ -1,20 +1,45 @@
|
||||
import getenv from 'getenv'
|
||||
|
||||
type Maybe<T> = T | null | undefined
|
||||
|
||||
// Where to fetch the remote "federated" schema. If you have a long-running branch
|
||||
// against a development schema, it's probably easiest to set this manually to "develop"
|
||||
export const CYPRESS_INTERNAL_CLOUD_ENV = getenv('CYPRESS_INTERNAL_CLOUD_ENV', 'staging') as 'production' | 'staging' | 'development'
|
||||
export const DEFAULT_INTERNAL_CLOUD_ENV = 'staging'
|
||||
|
||||
process.env.CYPRESS_INTERNAL_CLOUD_ENV = CYPRESS_INTERNAL_CLOUD_ENV
|
||||
export type MODES = 'dev' | 'devWatch' | 'test'
|
||||
|
||||
process.env.CYPRESS_KONFIG_ENV = getenv('CYPRESS_KONFIG_ENV', CYPRESS_INTERNAL_CLOUD_ENV)
|
||||
export const ENV_VARS = {
|
||||
// Building the "production" version of Cypress
|
||||
PROD: {
|
||||
CYPRESS_INTERNAL_ENV: 'production',
|
||||
CYPRESS_INTERNAL_CLOUD_ENV: 'production',
|
||||
CYPRESS_INTERNAL_GQL_PORT: `52200`,
|
||||
},
|
||||
|
||||
export const CYPRESS_INTERNAL_DEBUG_PORT_STARTUP = getenv.int('CYPRESS_INTERNAL_DEBUG_PORT_STARTUP', 7200)
|
||||
// Used when we're spawning Cypress as the E2E target for the
|
||||
// test runner. We build the assets w/ GQL_TEST_PORT into `dist-e2e`,
|
||||
// and spawn the server against
|
||||
E2E_TEST_TARGET: {
|
||||
CYPRESS_INTERNAL_CLOUD_ENV: DEFAULT_INTERNAL_CLOUD_ENV, // staging for now, until we get an e2e workflow w/ cloud project
|
||||
CYPRESS_INTERNAL_GQL_PORT: `52300`,
|
||||
CYPRESS_INTERNAL_ENV: 'staging', // Different than DEV, which will default to "development". TODO: Make this do less things internall
|
||||
CYPRESS_INTERNAL_E2E_TESTING_SELF: `true`,
|
||||
},
|
||||
|
||||
export const CYPRESS_INTERNAL_DEBUG_PORT_ELECTRON = getenv.int('CYPRESS_INTERNAL_DEBUG_PORT_ELECTRON', 7201)
|
||||
// Uses the "built" vite assets, not the served ones
|
||||
DEV_OPEN: {
|
||||
CYPRESS_KONFIG_ENV: DEFAULT_INTERNAL_CLOUD_ENV, // TODO: Change this / remove konfig
|
||||
CYPRESS_INTERNAL_CLOUD_ENV: DEFAULT_INTERNAL_CLOUD_ENV, // staging for now, until we get an e2e workflow w/ cloud project
|
||||
CYPRESS_INTERNAL_GQL_PORT: `52200`,
|
||||
},
|
||||
|
||||
export const CYPRESS_INTERNAL_DEBUG_PORT_CODEGEN = getenv.int('CYPRESS_INTERNAL_DEBUG_PORT_CODEGEN', 7202)
|
||||
// Used when we're running Cypress in true "development" mode
|
||||
DEV: {
|
||||
CYPRESS_KONFIG_ENV: DEFAULT_INTERNAL_CLOUD_ENV, // TODO: Change this / remove konfig
|
||||
CYPRESS_INTERNAL_CLOUD_ENV: DEFAULT_INTERNAL_CLOUD_ENV, // staging for now, until we get an e2e workflow w/ cloud project
|
||||
CYPRESS_INTERNAL_GQL_PORT: `52200`,
|
||||
CYPRESS_INTERNAL_VITE_APP_PORT: `3333`,
|
||||
CYPRESS_INTERNAL_VITE_LAUNCHPAD_PORT: `3001`,
|
||||
},
|
||||
}
|
||||
|
||||
interface GulpGlobalVals {
|
||||
debug?: Maybe<'--inspect' | '--inspect-brk'>
|
||||
|
||||
+232
-37
@@ -1,12 +1,28 @@
|
||||
/**
|
||||
* More information about our build process lives inside of the
|
||||
* CONTRIBUTING.md
|
||||
*
|
||||
* @summary Build pipeline for all new commands
|
||||
* @docs https://gulpjs.com
|
||||
* @usage `yarn gulp myTaskName` from the workspace root directory
|
||||
*/
|
||||
|
||||
import gulp from 'gulp'
|
||||
import { autobarrelWatcher } from './tasks/gulpAutobarrel'
|
||||
import { startCypressWatch } from './tasks/gulpCypress'
|
||||
import { startCypressWatch, startCypressForTest, openCypressLaunchpad, openCypressApp, runCypressLaunchpad, wrapRunWithExit, runCypressApp } from './tasks/gulpCypress'
|
||||
import { graphqlCodegen, graphqlCodegenWatch, nexusCodegen, nexusCodegenWatch, generateFrontendSchema, syncRemoteGraphQL } from './tasks/gulpGraphql'
|
||||
import { viteApp, viteBuildAndWatchLaunchpadForTest, viteBuildLaunchpadForTest, serveBuiltLaunchpadForTest, viteCleanApp, viteCleanLaunchpad, viteLaunchpad, serveBuiltAppForTest, viteBuildAppForTest, viteBuildAndWatchAppForTest, viteBuildApp, viteBuildLaunchpad } from './tasks/gulpVite'
|
||||
import { checkTs } from './tasks/gulpTsc'
|
||||
import { viteApp, viteCleanApp, viteCleanLaunchpad, viteLaunchpad } from './tasks/gulpVite'
|
||||
import { makePathMap } from './utils/makePathMap'
|
||||
import { setGulpGlobal } from './gulpConstants'
|
||||
import { makePackage } from './tasks/gulpMakePackage'
|
||||
import { setGulpGlobal } from './gulpConstants'
|
||||
import { exitAfterAll } from './tasks/gulpRegistry'
|
||||
import { execSync } from 'child_process'
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Local Development Workflow
|
||||
* * `yarn dev` is your primary command for getting work done
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
gulp.task(
|
||||
'codegen',
|
||||
@@ -14,6 +30,13 @@ gulp.task(
|
||||
// Autobarrel watcher
|
||||
autobarrelWatcher,
|
||||
|
||||
// Clean any vite assets
|
||||
gulp.parallel(
|
||||
viteCleanApp,
|
||||
viteCleanLaunchpad,
|
||||
),
|
||||
// Codegen for our GraphQL Server so we have the latest schema to build
|
||||
// the frontend codegen correctly
|
||||
// Fetch the latest "remote" schema from the Cypress cloud
|
||||
syncRemoteGraphQL,
|
||||
|
||||
@@ -30,33 +53,18 @@ gulp.task(
|
||||
gulp.series(
|
||||
'codegen',
|
||||
|
||||
gulp.parallel(
|
||||
// Clean the vite apps
|
||||
viteCleanApp,
|
||||
viteCleanLaunchpad,
|
||||
),
|
||||
|
||||
// Now that we have the codegen, we can start the frontend(s)
|
||||
gulp.parallel(
|
||||
viteApp,
|
||||
viteLaunchpad,
|
||||
),
|
||||
|
||||
// And we're finally ready for electron, watching for changes in /graphql to auto-restart the server
|
||||
// And we're finally ready for electron, watching for changes in
|
||||
// /graphql to auto-restart the server
|
||||
startCypressWatch,
|
||||
),
|
||||
)
|
||||
|
||||
gulp.task(
|
||||
'devNoWatch',
|
||||
gulp.series(
|
||||
async function setupDevNoWatch () {
|
||||
setGulpGlobal('shouldWatch', false)
|
||||
},
|
||||
'dev',
|
||||
),
|
||||
)
|
||||
|
||||
gulp.task(
|
||||
'debug',
|
||||
gulp.series(
|
||||
@@ -77,34 +85,168 @@ gulp.task(
|
||||
),
|
||||
)
|
||||
|
||||
gulp.task('buildProd', gulp.series(
|
||||
syncRemoteGraphQL,
|
||||
nexusCodegen,
|
||||
graphqlCodegen,
|
||||
))
|
||||
/**------------------------------------------------------------------------
|
||||
* Static Builds
|
||||
* Tasks that aren't watched. Usually composed together with other tasks.
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
gulp.task('buildProd',
|
||||
gulp.series(
|
||||
gulp.parallel(
|
||||
viteCleanApp,
|
||||
viteCleanLaunchpad,
|
||||
),
|
||||
|
||||
syncRemoteGraphQL,
|
||||
nexusCodegen,
|
||||
graphqlCodegen,
|
||||
|
||||
// Build the frontend(s) for production.
|
||||
gulp.parallel(
|
||||
viteBuildApp,
|
||||
viteBuildLaunchpad,
|
||||
),
|
||||
))
|
||||
|
||||
gulp.task(
|
||||
'postinstall',
|
||||
gulp.series(
|
||||
gulp.parallel(
|
||||
// Clean the vite apps
|
||||
viteCleanApp,
|
||||
viteCleanLaunchpad,
|
||||
),
|
||||
'buildProd',
|
||||
exitAfterAll,
|
||||
),
|
||||
)
|
||||
|
||||
// gulp.task(
|
||||
// 'devLegacy', // Tim: TODO
|
||||
// )
|
||||
/**------------------------------------------------------------------------
|
||||
* Launchpad Testing
|
||||
* This task builds and hosts the launchpad as if it was a static website.
|
||||
* In production, this app would be served over the file:// protocol via
|
||||
* the Electron app. However, when e2e testing the launchpad, we'll want to
|
||||
* visit it using cy.visit within our integration suites.
|
||||
*
|
||||
* * cyOpenLaunchpadE2E is for local dev and watches.
|
||||
* * cyRunLaunchpadE2E is meant to be run in CI and does not watch.
|
||||
* * cyOpenAppE2E is for local dev and watches.
|
||||
* * cyRunAppE2E is meant to be run in CI and does not watch.
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
// gulp.task(
|
||||
// 'debug', // Tim: TODO
|
||||
// )
|
||||
gulp.task('cyRunLaunchpadE2E', gulp.series(
|
||||
// 1. Build the Cypress App itself
|
||||
'buildProd',
|
||||
|
||||
// 2. Build the Launchpad under test.
|
||||
viteBuildLaunchpadForTest,
|
||||
|
||||
// 3. Host the Launchpad on a static server for cy.visit.
|
||||
serveBuiltLaunchpadForTest,
|
||||
|
||||
// 4. Start the TEST Cypress App, such that its ports and other globals
|
||||
// don't conflict with the real Cypress App.
|
||||
startCypressForTest,
|
||||
|
||||
// 5. Start the REAL Cypress App, which will execute the integration specs.
|
||||
async function _runCypressLaunchpad () {
|
||||
wrapRunWithExit(await runCypressLaunchpad())
|
||||
},
|
||||
))
|
||||
|
||||
gulp.task('cyRunAppE2E', gulp.series(
|
||||
// 1. Build the Cypress App itself
|
||||
'buildProd',
|
||||
|
||||
// 2. Build the Launchpad under test.
|
||||
gulp.parallel(
|
||||
viteBuildLaunchpadForTest,
|
||||
viteBuildAppForTest,
|
||||
),
|
||||
|
||||
// 3. Host the Launchpad on a static server for cy.visit.
|
||||
serveBuiltAppForTest,
|
||||
|
||||
// 4. Start the TEST Cypress App, such that its ports and other globals
|
||||
// don't conflict with the real Cypress App.
|
||||
startCypressForTest,
|
||||
|
||||
// 5. Start the REAL Cypress App, which will execute the integration specs.
|
||||
async function _runCypressApp () {
|
||||
wrapRunWithExit(await runCypressApp())
|
||||
},
|
||||
))
|
||||
|
||||
const cyOpenLaunchpad = gulp.series(
|
||||
// 2. Build + watch Launchpad under test.
|
||||
// This watches for changes and is not the same things as statically
|
||||
// building the app for production.
|
||||
viteBuildAndWatchLaunchpadForTest,
|
||||
|
||||
// 4. Start the TEST Cypress App, such that its ports and other globals
|
||||
// don't conflict with the real Cypress App.
|
||||
startCypressForTest,
|
||||
|
||||
// 3. Host the Launchpad on a static server for cy.visit.
|
||||
serveBuiltLaunchpadForTest,
|
||||
|
||||
// 5. Start the REAL (dev) Cypress App, which will launch in open mode.
|
||||
openCypressLaunchpad,
|
||||
)
|
||||
|
||||
const cyOpenApp = gulp.series(
|
||||
// 2. Build + watch Launchpad under test.
|
||||
// This watches for changes and is not the same things as statically
|
||||
// building the app for production.
|
||||
gulp.parallel(
|
||||
viteBuildLaunchpadForTest,
|
||||
viteBuildAndWatchAppForTest,
|
||||
),
|
||||
|
||||
// 3. Start the TEST Cypress App, such that its ports and other globals
|
||||
// don't conflict with the real Cypress App.
|
||||
startCypressForTest,
|
||||
|
||||
// 4. Host the Launchpad on a static server for cy.visit.
|
||||
serveBuiltAppForTest,
|
||||
|
||||
// 5. Start the REAL (dev) Cypress App, which will launch in open mode.
|
||||
openCypressApp,
|
||||
)
|
||||
|
||||
// Open Cypress in production mode.
|
||||
// Rebuild the Launchpad app between changes.
|
||||
gulp.task('cyOpenLaunchpadE2E', gulp.series(
|
||||
// 1. Build the Cypress App itself
|
||||
'buildProd',
|
||||
|
||||
// 2. Open the "app"
|
||||
cyOpenLaunchpad,
|
||||
))
|
||||
|
||||
// Open Cypress in production mode.
|
||||
// Rebuild the Launchpad app between changes.
|
||||
gulp.task('cyOpenAppE2E', gulp.series(
|
||||
// 1. Build the Cypress App itself
|
||||
'buildProd',
|
||||
|
||||
// 2. Open the launchpad app
|
||||
cyOpenApp,
|
||||
))
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Utilities
|
||||
* checkTs: Runs `check-ts` in each of the packages & prints errors when
|
||||
* all are completed
|
||||
*
|
||||
* makePackage: Scaffolds a new package in the packages/ directory
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
gulp.task(makePackage)
|
||||
gulp.task(checkTs)
|
||||
gulp.task(makePackage)
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Internal / Test / Debug
|
||||
*
|
||||
* Tasks that are typically composed into other workflows, but are exposed
|
||||
* here for debugging, e.g. `yarn gulp syncRemoteGraphQL`
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
gulp.task(syncRemoteGraphQL)
|
||||
gulp.task(generateFrontendSchema)
|
||||
gulp.task(makePathMap)
|
||||
@@ -112,3 +254,56 @@ gulp.task(nexusCodegen)
|
||||
gulp.task(nexusCodegenWatch)
|
||||
gulp.task(graphqlCodegen)
|
||||
gulp.task(graphqlCodegenWatch)
|
||||
gulp.task(startCypressForTest)
|
||||
|
||||
gulp.task(viteCleanApp)
|
||||
gulp.task(viteCleanLaunchpad)
|
||||
|
||||
gulp.task(viteBuildLaunchpadForTest)
|
||||
gulp.task(viteBuildAppForTest)
|
||||
|
||||
gulp.task(serveBuiltAppForTest)
|
||||
gulp.task(serveBuiltLaunchpadForTest)
|
||||
|
||||
gulp.task(viteBuildAndWatchLaunchpadForTest)
|
||||
gulp.task(viteBuildAndWatchAppForTest)
|
||||
|
||||
gulp.task(viteBuildApp)
|
||||
gulp.task(viteBuildLaunchpad)
|
||||
|
||||
gulp.task('debugCypressLaunchpad', gulp.series(
|
||||
async function setupDebugBrk () {
|
||||
setGulpGlobal('debug', '--inspect-brk')
|
||||
},
|
||||
openCypressLaunchpad,
|
||||
))
|
||||
|
||||
gulp.task(startCypressWatch)
|
||||
gulp.task(openCypressApp)
|
||||
gulp.task(openCypressLaunchpad)
|
||||
|
||||
// If we want to run individually, for debugging/testing
|
||||
gulp.task('cyOpenLaunchpadOnly', cyOpenLaunchpad)
|
||||
gulp.task('cyOpenAppOnly', cyOpenApp)
|
||||
|
||||
// Tapping into:
|
||||
// https://github.com/gulpjs/gulp-cli/blob/da8241ecbacd59158deaa5471ff8a7f43901a94b/lib/versioned/%5E4.0.0/log/sync-task.js#L21-L27
|
||||
const gulplog = require('gulplog')
|
||||
|
||||
let didntExitCorrectly = false
|
||||
const warn = gulplog.warn
|
||||
|
||||
gulplog.warn = function (...args: string[]) {
|
||||
if (args.some((a) => String(a).includes('forget to signal async completion'))) {
|
||||
didntExitCorrectly = true
|
||||
}
|
||||
|
||||
return warn.apply(this, arguments)
|
||||
}
|
||||
|
||||
process.on('exit', () => {
|
||||
if (didntExitCorrectly) {
|
||||
execSync('killall node')
|
||||
process.exitCode = 1
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,63 +1,135 @@
|
||||
/**
|
||||
* How the Cypress backend is started and watched. Formerly
|
||||
* `node scripts/cypress.js open` or `node scripts/cypress.js run`
|
||||
*
|
||||
* @summary Gulp tasks to run the Cypress app.
|
||||
*/
|
||||
|
||||
import chokidar from 'chokidar'
|
||||
import path from 'path'
|
||||
import childProcess, { ChildProcess } from 'child_process'
|
||||
import pDefer from 'p-defer'
|
||||
|
||||
import { monorepoPaths } from '../monorepoPaths'
|
||||
import { getGulpGlobal } from '../gulpConstants'
|
||||
import { ENV_VARS, getGulpGlobal } from '../gulpConstants'
|
||||
import { forked } from '../utils/childProcessUtils'
|
||||
import { exitAndRemoveProcess } from './gulpRegistry'
|
||||
import type { ChildProcess } from 'child_process'
|
||||
|
||||
/**
|
||||
* Starts cypress, but watches the GraphQL files & restarts the server
|
||||
* when any of those change
|
||||
*/
|
||||
export function startCypressWatch () {
|
||||
const shouldWatch = getGulpGlobal('shouldWatch')
|
||||
const pathToCli = path.resolve(monorepoPaths.root, 'cli', 'bin', 'cypress')
|
||||
|
||||
const watcher = shouldWatch ? chokidar.watch([
|
||||
/**------------------------------------------------------------------------
|
||||
* Cypress CLI
|
||||
* Starts Cypress, like a user would.
|
||||
* * openCypress - Normal `cypress open` command
|
||||
* * runCypress - Normal `cypress run` command
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
export async function openCypressLaunchpad () {
|
||||
return spawnCypressWithMode('open', 'dev', ENV_VARS.DEV_OPEN, ['--project', monorepoPaths.pkgLaunchpad])
|
||||
}
|
||||
|
||||
export async function openCypressApp () {
|
||||
return spawnCypressWithMode('open', 'dev', ENV_VARS.DEV_OPEN, ['--project', monorepoPaths.pkgApp])
|
||||
}
|
||||
|
||||
export async function runCypressLaunchpad () {
|
||||
return spawnCypressWithMode('run', 'dev', ENV_VARS.PROD, ['--project', monorepoPaths.pkgLaunchpad])
|
||||
}
|
||||
|
||||
export async function runCypressApp () {
|
||||
return spawnCypressWithMode('run', 'dev', ENV_VARS.PROD, ['--project', monorepoPaths.pkgApp])
|
||||
}
|
||||
|
||||
export async function runCypressProd () {
|
||||
return spawnCypressWithMode('run', 'prod', ENV_VARS.PROD)
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Testing Tasks
|
||||
* Building and running the Cypress app and graphql server for testing.
|
||||
* * startCypressForTest - Start the Cypress server, but without watching
|
||||
* * runCypressAgainstDist - Serve the dist'd frontend over file://
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
// Use the GQL Test Port (52300 by default, defined in ./gulp/gulpConstants)
|
||||
// Spawns Cypress in "Test Cypress within Cypress" mode
|
||||
export async function startCypressForTest () {
|
||||
return spawnCypressWithMode('open', 'test', ENV_VARS.E2E_TEST_TARGET)
|
||||
}
|
||||
|
||||
export async function runCypressAgainstDist () {
|
||||
return spawnCypressWithMode('run', 'test', ENV_VARS.E2E_TEST_TARGET)
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Start and Watch Utils
|
||||
* * spawnCypressWithMode - Formerly known as: `node ./scripts/cypress.js run`
|
||||
* * watchCypress - Watch the dev server and graphql files
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
async function spawnCypressWithMode (
|
||||
mode: 'open' | 'run',
|
||||
type: 'dev' | 'prod' | 'test',
|
||||
env: Record<string, string> = {},
|
||||
additionalArgv: string[] = [],
|
||||
) {
|
||||
let argv = process.argv.slice(3).concat(additionalArgv)
|
||||
|
||||
const debugFlag = getGulpGlobal('debug')
|
||||
|
||||
if (debugFlag) {
|
||||
env = { ...env, CYPRESS_INTERNAL_DEV_DEBUG: debugFlag }
|
||||
}
|
||||
|
||||
if (mode === 'open') {
|
||||
if (!argv.includes('--project') && !argv.includes('--global')) {
|
||||
argv.push('--global')
|
||||
}
|
||||
|
||||
// If we've passed --record, it's for a "run" mode, probably in the same pipeline.
|
||||
if (argv.includes('--record')) {
|
||||
argv = argv.slice(0, argv.indexOf('--record'))
|
||||
}
|
||||
}
|
||||
|
||||
if (!argv.includes('--dev')) {
|
||||
argv.push('--dev')
|
||||
}
|
||||
|
||||
const finalEnv = {
|
||||
...process.env,
|
||||
...env,
|
||||
LAUNCHPAD: '1',
|
||||
}
|
||||
|
||||
return await forked(`cy:${mode}:${type}`, pathToCli, [mode, ...argv], {
|
||||
env: finalEnv,
|
||||
waitForData: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Watch Commands
|
||||
* Starts Cypress, but watches the GraphQL files, and restarts the server.
|
||||
* * startCypressWatch - Normal `cypress open` command, with watching
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
export async function startCypressWatch () {
|
||||
const watcher = chokidar.watch([
|
||||
'packages/graphql/src/**/*.{js,ts}',
|
||||
'packages/server/lib/graphql/**/*.{js,ts}',
|
||||
], {
|
||||
cwd: monorepoPaths.root,
|
||||
ignored: /\.gen\.ts/,
|
||||
ignoreInitial: true,
|
||||
}) : null
|
||||
|
||||
let child: ChildProcess | null = null
|
||||
})
|
||||
|
||||
let isClosing = false
|
||||
let isRestarting = false
|
||||
let child: ChildProcess | null = null
|
||||
|
||||
const argv = process.argv.slice(3)
|
||||
const pathToCli = path.resolve(monorepoPaths.root, 'cli', 'bin', 'cypress')
|
||||
|
||||
function openServer () {
|
||||
if (child) {
|
||||
child.removeAllListeners()
|
||||
}
|
||||
|
||||
if (!argv.includes('--project') && !argv.includes('--global')) {
|
||||
argv.push('--global')
|
||||
}
|
||||
|
||||
if (!argv.includes('--dev')) {
|
||||
argv.push('--dev')
|
||||
}
|
||||
|
||||
const debugFlag = getGulpGlobal('debug')
|
||||
|
||||
if (debugFlag) {
|
||||
process.env.CYPRESS_INTERNAL_DEV_DEBUG = debugFlag
|
||||
}
|
||||
|
||||
child = childProcess.fork(pathToCli, ['open', ...argv], {
|
||||
stdio: 'inherit',
|
||||
execArgv: [],
|
||||
env: {
|
||||
...process.env,
|
||||
LAUNCHPAD: '1',
|
||||
CYPRESS_INTERNAL_DEV_WATCH: shouldWatch ? 'true' : undefined,
|
||||
},
|
||||
})
|
||||
async function startCypressWithListeners () {
|
||||
child = await spawnCypressWithMode('open', 'dev', ENV_VARS.DEV)
|
||||
|
||||
child.on('exit', (code) => {
|
||||
if (isClosing) {
|
||||
@@ -78,27 +150,49 @@ export function startCypressWatch () {
|
||||
const dfd = pDefer()
|
||||
|
||||
if (child) {
|
||||
child.on('exit', dfd.resolve)
|
||||
isRestarting = true
|
||||
child.send('close')
|
||||
child.on('exit', dfd.resolve)
|
||||
await exitAndRemoveProcess(child)
|
||||
} else {
|
||||
dfd.resolve()
|
||||
}
|
||||
|
||||
await dfd.promise
|
||||
|
||||
if (child) {
|
||||
child.removeAllListeners()
|
||||
}
|
||||
|
||||
await startCypressWithListeners()
|
||||
isRestarting = false
|
||||
openServer()
|
||||
}
|
||||
|
||||
if (shouldWatch) {
|
||||
watcher?.on('add', restartServer)
|
||||
watcher?.on('change', restartServer)
|
||||
}
|
||||
watcher.on('add', restartServer)
|
||||
watcher.on('change', restartServer)
|
||||
|
||||
openServer()
|
||||
await startCypressWithListeners()
|
||||
|
||||
process.on('beforeExit', () => {
|
||||
isClosing = true
|
||||
child?.send('close')
|
||||
watcher.close()
|
||||
})
|
||||
}
|
||||
|
||||
export function wrapRunWithExit (proc: ChildProcess) {
|
||||
function killAndExit (code: number) {
|
||||
process.exit(code)
|
||||
}
|
||||
|
||||
proc.on('exit', (code) => {
|
||||
killAndExit(code ?? 0)
|
||||
})
|
||||
|
||||
proc.on('error', (err) => {
|
||||
console.error({ err })
|
||||
killAndExit(1)
|
||||
})
|
||||
|
||||
proc.on('disconnect', () => {
|
||||
console.error('disconnected')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { nexusTypegen, watchNexusTypegen } from '../utils/nexusTypegenUtil'
|
||||
import { monorepoPaths } from '../monorepoPaths'
|
||||
import { spawned } from '../utils/childProcessUtils'
|
||||
import { spawn } from 'child_process'
|
||||
import { CYPRESS_INTERNAL_CLOUD_ENV } from '../gulpConstants'
|
||||
import { DEFAULT_INTERNAL_CLOUD_ENV } from '../gulpConstants'
|
||||
|
||||
export async function nexusCodegen () {
|
||||
return nexusTypegen({
|
||||
@@ -89,12 +89,12 @@ const ENV_MAP = {
|
||||
}
|
||||
|
||||
export async function syncRemoteGraphQL () {
|
||||
if (!ENV_MAP[CYPRESS_INTERNAL_CLOUD_ENV]) {
|
||||
if (!ENV_MAP[DEFAULT_INTERNAL_CLOUD_ENV]) {
|
||||
throw new Error(`Expected --env to be one of ${Object.keys(ENV_MAP).join(', ')}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await rp.get(`${ENV_MAP[CYPRESS_INTERNAL_CLOUD_ENV]}/test-runner-graphql-schema`)
|
||||
const body = await rp.get(`${ENV_MAP[DEFAULT_INTERNAL_CLOUD_ENV]}/test-runner-graphql-schema`)
|
||||
|
||||
// TODO(tim): fix
|
||||
await fs.ensureDir(path.join(monorepoPaths.pkgGraphql, 'src/gen'))
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { ChildProcess } from 'child_process'
|
||||
import pDefer from 'p-defer'
|
||||
import treeKill from 'tree-kill'
|
||||
|
||||
const childProcesses = new Set<ChildProcess>()
|
||||
const exitedPids = new Set<number>()
|
||||
|
||||
let hasExited = false
|
||||
|
||||
export function addChildProcess (child: ChildProcess) {
|
||||
if (hasExited) {
|
||||
treeKill(child.pid)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
childProcesses.add(child)
|
||||
child.on('exit', () => {
|
||||
if (!hasExited) {
|
||||
exitAndRemoveProcess(child)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function exitAndRemoveProcess (child: ChildProcess) {
|
||||
if (exitedPids.has(child.pid)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!childProcesses.has(child)) {
|
||||
throw new Error(`Cannot remove child process ${child.pid}, it was never registered`)
|
||||
}
|
||||
|
||||
childProcesses.delete(child)
|
||||
|
||||
const dfd = pDefer()
|
||||
|
||||
exitedPids.add(child.pid)
|
||||
treeKill(child.pid, (err) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
dfd.resolve()
|
||||
})
|
||||
|
||||
return dfd.promise
|
||||
}
|
||||
|
||||
export async function exitAllProcesses () {
|
||||
await Promise.all(Array.from(childProcesses).map(exitAndRemoveProcess))
|
||||
}
|
||||
|
||||
process.stdin.resume() //so the program will not close instantly
|
||||
|
||||
export async function exitAfterAll () {
|
||||
process.stdin.pause()
|
||||
}
|
||||
|
||||
function exitHandler (msg: string) {
|
||||
return async function _exitHandler (exitCode: number) {
|
||||
hasExited = true
|
||||
console.log(`Exiting due to ${msg} => code ${exitCode}`)
|
||||
await exitAllProcesses()
|
||||
process.exit(exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
// do something when app is closing
|
||||
process.on('exit', exitHandler('exit'))
|
||||
|
||||
// catches ctrl+c event
|
||||
process.on('SIGINT', exitHandler('SIGINT'))
|
||||
|
||||
// catches "kill pid" (for example: nodemon restart)
|
||||
process.on('SIGUSR1', exitHandler('SIGUSR1'))
|
||||
process.on('SIGUSR2', exitHandler('SIGUSR2'))
|
||||
|
||||
// catches uncaught exceptions
|
||||
process.on('uncaughtException', exitHandler('uncaughtException'))
|
||||
+168
-27
@@ -1,21 +1,104 @@
|
||||
import type { SpawnOptions } from 'child_process'
|
||||
import getenv from 'getenv'
|
||||
import pDefer from 'p-defer'
|
||||
import { monorepoPaths } from '../monorepoPaths'
|
||||
import { AllSpawnableApps, spawned } from '../utils/childProcessUtils'
|
||||
/**
|
||||
* The Launchpad and App clients are both built with Vite and largely
|
||||
* share the same test pipeline and build commands.
|
||||
*
|
||||
* @summary Build pipeline for the Vite frontend(s):
|
||||
* @packages/launchpad + @packages/app
|
||||
* @docs https://vitejs.dev
|
||||
*/
|
||||
|
||||
const CYPRESS_VITE_APP_PORT = getenv.int('CYPRESS_VITE_APP_PORT', 3333)
|
||||
const CYPRESS_VITE_LAUNCHPAD_PORT = getenv.int('CYPRESS_VITE_LAUNCHPAD_PORT', 3001)
|
||||
import type { SpawnOptions } from 'child_process'
|
||||
import { ENV_VARS } from '../gulpConstants'
|
||||
|
||||
import { monorepoPaths } from '../monorepoPaths'
|
||||
import { AllSpawnableApps, spawned, spawnUntilMatch } from '../utils/childProcessUtils'
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Local Development Workflow
|
||||
* Spawn the Vite frontend dev servers in watch mode.
|
||||
* * viteApp
|
||||
* * viteLaunchpad
|
||||
* * watchViteBuild
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
export function viteApp () {
|
||||
return viteDev('vite-app', `vite --port ${CYPRESS_VITE_APP_PORT} --base /__vite__/`, {
|
||||
const GQL_PORT = ENV_VARS.DEV.CYPRESS_INTERNAL_GQL_PORT
|
||||
const APP_PORT = ENV_VARS.DEV.CYPRESS_INTERNAL_VITE_APP_PORT
|
||||
|
||||
return spawnViteDevServer('vite-app', `yarn vite --port ${APP_PORT} --base /__vite__/`, {
|
||||
cwd: monorepoPaths.pkgApp,
|
||||
env: {
|
||||
VITE_CYPRESS_INTERNAL_GQL_PORT: GQL_PORT,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function viteLaunchpad () {
|
||||
return viteDev('vite-launchpad', `vite --port ${CYPRESS_VITE_LAUNCHPAD_PORT}`, {
|
||||
const GQL_PORT = ENV_VARS.DEV.CYPRESS_INTERNAL_GQL_PORT
|
||||
const LAUNCHPAD_PORT = ENV_VARS.DEV.CYPRESS_INTERNAL_VITE_LAUNCHPAD_PORT
|
||||
|
||||
return spawnViteDevServer('vite-launchpad', `yarn vite --port ${LAUNCHPAD_PORT}`, {
|
||||
cwd: monorepoPaths.pkgLaunchpad,
|
||||
env: {
|
||||
VITE_CYPRESS_INTERNAL_GQL_PORT: GQL_PORT,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// This watcher task is generally used within cypress:open when running in
|
||||
// end-to-end mode.
|
||||
function watchViteBuild (
|
||||
prefix: AllSpawnableApps,
|
||||
command: string,
|
||||
options: SpawnOptions = {},
|
||||
) {
|
||||
// This will match strings like "built in 200ms" and "built in 5s"
|
||||
return spawnUntilMatch(prefix, {
|
||||
command,
|
||||
match: /built in (\d+)(m?s)/i,
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
function spawnViteDevServer (
|
||||
prefix: AllSpawnableApps,
|
||||
command: string,
|
||||
options: SpawnOptions = {},
|
||||
) {
|
||||
return spawnUntilMatch(prefix, {
|
||||
command,
|
||||
match: 'dev server running at',
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Build Tasks
|
||||
* Build the Vite frontend(s) for production to be served by the Launchpad
|
||||
* and App. Generally used in CI.
|
||||
* * viteBuildApp
|
||||
* * viteBuildLaunchpad
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
export function viteBuildApp () {
|
||||
return spawned('vite:build-app', `yarn vite build`, {
|
||||
cwd: monorepoPaths.pkgApp,
|
||||
waitForExit: true,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function viteBuildLaunchpad () {
|
||||
return spawned('vite:build-launchpad', `yarn vite build`, {
|
||||
cwd: monorepoPaths.pkgLaunchpad,
|
||||
waitForExit: true,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,25 +116,83 @@ export function viteCleanLaunchpad () {
|
||||
})
|
||||
}
|
||||
|
||||
function viteDev (
|
||||
prefix: AllSpawnableApps,
|
||||
command: string,
|
||||
opts: SpawnOptions = {},
|
||||
/**------------------------------------------------------------------------
|
||||
* Testing Tasks
|
||||
* Build and serve the Vite frontend(s) as web apps on a static server.
|
||||
* * viteBuildLaunchpadForTest
|
||||
* * viteBuildAppForTest
|
||||
* * serveBuiltLaunchpadForTest
|
||||
* * serveBuiltAppForTest
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
) {
|
||||
const dfd = pDefer()
|
||||
let ready = false
|
||||
|
||||
spawned(prefix, command, opts, {
|
||||
tapOut (chunk, enc, cb) {
|
||||
if (!ready && String(chunk).includes('dev server running at')) {
|
||||
ready = true
|
||||
setTimeout(() => dfd.resolve(), 20) // flush the rest of the chunks
|
||||
}
|
||||
|
||||
cb(null, chunk)
|
||||
// After running `serveBuiltLaunchpadForTest`, you're able to visit
|
||||
// `http://localhost:5555` to access the Launchpad frontend.
|
||||
export function serveBuiltLaunchpadForTest () {
|
||||
return spawnUntilMatch('serve:launchpad-for-test', {
|
||||
command: `yarn serve ./dist-e2e -p 5555`,
|
||||
match: 'Accepting connections',
|
||||
options: {
|
||||
cwd: monorepoPaths.pkgLaunchpad,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function viteBuildLaunchpadForTest () {
|
||||
const GQL_PORT = ENV_VARS.E2E_TEST_TARGET.CYPRESS_INTERNAL_GQL_PORT
|
||||
|
||||
return spawned('vite:build-launchpad-for-test', `yarn vite build --outDir=./dist-e2e`, {
|
||||
cwd: monorepoPaths.pkgLaunchpad,
|
||||
waitForExit: true,
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
VITE_CYPRESS_INTERNAL_GQL_PORT: GQL_PORT,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function viteBuildAndWatchLaunchpadForTest () {
|
||||
const GQL_PORT = ENV_VARS.E2E_TEST_TARGET.CYPRESS_INTERNAL_GQL_PORT
|
||||
|
||||
return watchViteBuild('vite:build-watch-launchpad-for-test', `yarn vite build --watch --outDir=./dist-e2e`, {
|
||||
cwd: monorepoPaths.pkgLaunchpad,
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
VITE_CYPRESS_INTERNAL_GQL_PORT: GQL_PORT,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**----------------------
|
||||
*todo Implement E2E tests for the App.
|
||||
*------------------------**/
|
||||
|
||||
export function viteBuildAppForTest () {
|
||||
const GQL_PORT = ENV_VARS.E2E_TEST_TARGET.CYPRESS_INTERNAL_GQL_PORT
|
||||
|
||||
return spawned('vite:build-app-for-test', `yarn vite build --outDir=./dist-e2e`, {
|
||||
cwd: monorepoPaths.pkgApp,
|
||||
waitForExit: true,
|
||||
env: {
|
||||
VITE_CYPRESS_INTERNAL_GQL_PORT: GQL_PORT,
|
||||
...process.env,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function serveBuiltAppForTest () {
|
||||
return spawned('serve:app-for-test', `yarn serve ./dist-e2e -p 5556`, {
|
||||
cwd: monorepoPaths.pkgApp,
|
||||
})
|
||||
}
|
||||
|
||||
export async function viteBuildAndWatchAppForTest () {
|
||||
const GQL_PORT = ENV_VARS.E2E_TEST_TARGET.CYPRESS_INTERNAL_GQL_PORT
|
||||
|
||||
return watchViteBuild('vite:build-watch-app-for-test', `yarn vite build --watch --outDir=./dist-e2e`, {
|
||||
cwd: monorepoPaths.pkgApp,
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
VITE_CYPRESS_INTERNAL_GQL_PORT: GQL_PORT,
|
||||
},
|
||||
})
|
||||
|
||||
return dfd.promise
|
||||
}
|
||||
|
||||
@@ -1,50 +1,92 @@
|
||||
import { exec, ExecOptions, spawn, SpawnOptions } from 'child_process'
|
||||
import { ChildProcess, exec, ExecOptions, fork, ForkOptions, spawn, SpawnOptions } from 'child_process'
|
||||
import through2 from 'through2'
|
||||
import pDefer from 'p-defer'
|
||||
import util from 'util'
|
||||
|
||||
// import psTree from 'ps-tree'
|
||||
// import psNode from 'ps-node'
|
||||
// import util from 'util'
|
||||
|
||||
import { prefixStream } from './prefixStream'
|
||||
|
||||
const spawningApps = new Set()
|
||||
// const killAsync = util.promisify(psNode.kill)
|
||||
// const psTreeAsync = util.promisify(psTree)
|
||||
// const runningApps = new Map<
|
||||
// AllSpawnableApps,
|
||||
// [ChildProcess, ArgsFor<typeof spawned>, Function]
|
||||
// >()
|
||||
|
||||
export async function allReady () {
|
||||
while (spawningApps.size > 0) {
|
||||
await new Promise((ready) => setTimeout(ready, 100))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
import { prefixLog, prefixStream } from './prefixStream'
|
||||
import { addChildProcess } from '../tasks/gulpRegistry'
|
||||
|
||||
export type AllSpawnableApps =
|
||||
| `vite-${string}`
|
||||
| `vite:build-${string}`
|
||||
| `serve:${string}`
|
||||
| 'gql-codegen'
|
||||
| `cy:${string}`
|
||||
|
||||
interface SpawnedOptions extends SpawnOptions {
|
||||
interface TapThroughConfig {
|
||||
tapOut?: through2.TransformFunction
|
||||
tapErr?: through2.TransformFunction
|
||||
}
|
||||
|
||||
interface SpawnedOptions extends TapThroughConfig, SpawnOptions {
|
||||
waitForExit?: boolean
|
||||
waitForData?: boolean
|
||||
}
|
||||
|
||||
interface SpawnUntilMatchConfig {
|
||||
command: string
|
||||
match: string | RegExp
|
||||
options?: SpawnOptions
|
||||
}
|
||||
|
||||
export async function spawnUntilMatch (
|
||||
prefix: AllSpawnableApps,
|
||||
config: SpawnUntilMatchConfig,
|
||||
) {
|
||||
const dfd = pDefer()
|
||||
let ready = false
|
||||
|
||||
spawned(prefix, config.command, {
|
||||
...config.options,
|
||||
tapOut (chunk, enc, cb) {
|
||||
if (!ready && String(chunk).match(config.match)) {
|
||||
ready = true
|
||||
setTimeout(() => dfd.resolve(), 20) // flush the rest of the chunks
|
||||
}
|
||||
|
||||
cb(null, chunk)
|
||||
},
|
||||
})
|
||||
|
||||
return dfd.promise
|
||||
}
|
||||
|
||||
interface ForkUntilMatchConfig {
|
||||
modulePath: string
|
||||
args: string[]
|
||||
match: string | RegExp
|
||||
options?: SpawnOptions
|
||||
}
|
||||
|
||||
export async function forkUntilMatch (
|
||||
prefix: AllSpawnableApps,
|
||||
config: ForkUntilMatchConfig,
|
||||
) {
|
||||
const dfd = pDefer<ChildProcess>()
|
||||
let ready = false
|
||||
|
||||
const cp = await forked(prefix, config.modulePath, config.args, {
|
||||
...config.options,
|
||||
tapOut (chunk, enc, cb) {
|
||||
if (!ready && String(chunk).match(config.match)) {
|
||||
ready = true
|
||||
setTimeout(() => dfd.resolve(cp), 20) // flush the rest of the chunks
|
||||
}
|
||||
|
||||
cb(null, chunk)
|
||||
},
|
||||
})
|
||||
|
||||
return dfd.promise
|
||||
}
|
||||
|
||||
export async function spawned (
|
||||
prefix: AllSpawnableApps,
|
||||
command: string,
|
||||
opts: SpawnedOptions = {},
|
||||
tapThrough: {
|
||||
tapOut?: through2.TransformFunction
|
||||
tapErr?: through2.TransformFunction
|
||||
} = {},
|
||||
) {
|
||||
const { waitForExit, ...spawnOpts } = opts
|
||||
const { waitForExit, waitForData, tapErr, tapOut, ...spawnOpts } = opts
|
||||
|
||||
spawningApps.add(prefix)
|
||||
const [executable, ...rest] = command.split(' ')
|
||||
let useExecutable = executable
|
||||
|
||||
@@ -52,94 +94,81 @@ export async function spawned (
|
||||
useExecutable = `${executable}.cmd`
|
||||
}
|
||||
|
||||
// console.log(useExecutable, rest, spawnOpts)
|
||||
|
||||
const cp = spawn(useExecutable, rest, {
|
||||
stdio: 'pipe',
|
||||
...spawnOpts,
|
||||
env: {
|
||||
FORCE_COLOR: '1',
|
||||
NODE_ENV: 'development',
|
||||
...process.env,
|
||||
...spawnOpts.env,
|
||||
},
|
||||
...spawnOpts,
|
||||
})
|
||||
const tapOut = tapThrough.tapOut || null
|
||||
const tapErr = tapThrough.tapErr || null
|
||||
const prefixedStdout = cp.stdout?.pipe(
|
||||
through2(function (chunk, enc, cb) {
|
||||
if (tapOut) {
|
||||
tapOut.call(this, chunk, enc, cb)
|
||||
} else {
|
||||
cb(null, chunk)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.pipe(prefixStream(`${prefix}:${cp.pid}`))
|
||||
|
||||
const prefixedStderr = cp.stderr?.pipe(
|
||||
through2(function (chunk, enc, cb) {
|
||||
if (tapErr) {
|
||||
tapErr.call(this, chunk, enc, cb)
|
||||
} else {
|
||||
cb(null, chunk)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.pipe(prefixStream(`${prefix}:${cp.pid}`))
|
||||
addChildProcess(cp)
|
||||
|
||||
prefixedStdout?.pipe(process.stdout)
|
||||
prefixedStderr?.pipe(process.stderr)
|
||||
return streamHandler(cp, {
|
||||
tapErr,
|
||||
tapOut,
|
||||
command,
|
||||
prefix,
|
||||
waitForExit,
|
||||
waitForData,
|
||||
})
|
||||
}
|
||||
|
||||
// const cleanup = () => {
|
||||
// prefixedStdout?.unpipe(process.stdout)
|
||||
// prefixedStderr?.unpipe(process.stderr)
|
||||
interface ForkedOptions extends TapThroughConfig, ForkOptions {
|
||||
waitForExit?: boolean
|
||||
waitForData?: boolean
|
||||
}
|
||||
|
||||
export async function forked (
|
||||
prefix: AllSpawnableApps,
|
||||
modulePath: string,
|
||||
args: string[],
|
||||
opts: ForkedOptions = {},
|
||||
) {
|
||||
const { waitForExit, waitForData, tapErr, tapOut, ...spawnOpts } = opts
|
||||
|
||||
// console.log(args)
|
||||
|
||||
// let useExecutable = executable
|
||||
// if (process.platform === 'win32' && !useExecutable.endsWith('.cmd')) {
|
||||
// useExecutable = `${executable}.cmd`
|
||||
// }
|
||||
|
||||
// runningApps.set(prefix, [cp, [prefix, command, opts], cleanup])
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (waitForExit) {
|
||||
if (process.platform === 'win32') {
|
||||
cp.on('exit', (code, signal) => {
|
||||
console.log(`Exit code: ${code} => ${signal}`)
|
||||
resolve(cp)
|
||||
})
|
||||
} else {
|
||||
cp.once('exit', (code, signal) => {
|
||||
console.log(`Exit code: ${code} => ${signal}`)
|
||||
resolve(cp)
|
||||
})
|
||||
}
|
||||
|
||||
cp.once('error', (e) => {
|
||||
console.log(`error executing ${command}`, e)
|
||||
reject(e)
|
||||
})
|
||||
} else {
|
||||
if (process.platform === 'win32') {
|
||||
cp.on('exit', (code, signal) => {
|
||||
console.log(`Exit code: ${code} => ${signal}`)
|
||||
})
|
||||
} else {
|
||||
cp.once('exit', (code, signal) => {
|
||||
console.log(`Exit code: ${code} => ${signal}`)
|
||||
})
|
||||
}
|
||||
|
||||
cp.once('error', (e) => {
|
||||
console.log(`error executing ${command}`, e)
|
||||
reject(e)
|
||||
})
|
||||
|
||||
cp.stdout?.once('data', () => {
|
||||
spawningApps.delete(prefix)
|
||||
resolve(cp)
|
||||
})
|
||||
}
|
||||
const cp = fork(modulePath, args, {
|
||||
stdio: 'pipe',
|
||||
...spawnOpts,
|
||||
env: {
|
||||
FORCE_COLOR: '1',
|
||||
NODE_ENV: 'development',
|
||||
...process.env,
|
||||
...spawnOpts.env,
|
||||
},
|
||||
})
|
||||
|
||||
addChildProcess(cp)
|
||||
|
||||
return streamHandler(cp, {
|
||||
tapOut,
|
||||
tapErr,
|
||||
command: modulePath,
|
||||
prefix,
|
||||
waitForExit,
|
||||
waitForData,
|
||||
})
|
||||
}
|
||||
|
||||
function writeError (e: Error) {
|
||||
return JSON.stringify({ name: e.name, message: e.message, stack: e.stack })
|
||||
}
|
||||
|
||||
const execAsyncLocal = util.promisify(exec)
|
||||
|
||||
interface ExecAsyncOptions extends ExecOptions {
|
||||
export interface ExecAsyncOptions extends ExecOptions {
|
||||
encoding?: string | null
|
||||
silent?: boolean
|
||||
}
|
||||
@@ -166,3 +195,76 @@ export const execAsync = async (
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export interface StreamHandlerConfig extends TapThroughConfig {
|
||||
prefix: string
|
||||
waitForExit?: boolean
|
||||
waitForData?: boolean
|
||||
command: string
|
||||
}
|
||||
|
||||
function streamHandler (cp: ChildProcess, config: StreamHandlerConfig) {
|
||||
const dfd = pDefer<ChildProcess>()
|
||||
const { command, tapErr = null, tapOut = null, prefix, waitForExit, waitForData = true } = config
|
||||
const prefixedStdout = cp.stdout?.pipe(
|
||||
through2(function (chunk, enc, cb) {
|
||||
if (tapOut) {
|
||||
tapOut.call(this, chunk, enc, cb)
|
||||
} else {
|
||||
cb(null, chunk)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.pipe(prefixStream(`${prefix}:${cp.pid}`))
|
||||
|
||||
const prefixedStderr = cp.stderr?.pipe(
|
||||
through2(function (chunk, enc, cb) {
|
||||
if (tapErr) {
|
||||
tapErr.call(this, chunk, enc, cb)
|
||||
} else {
|
||||
cb(null, chunk)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.pipe(prefixStream(`${prefix}:${cp.pid}`))
|
||||
|
||||
prefixedStdout?.pipe(process.stdout)
|
||||
prefixedStderr?.pipe(process.stderr)
|
||||
|
||||
const log = prefixLog(`${prefix}:${cp.pid}`)
|
||||
|
||||
if (waitForExit) {
|
||||
cp.on('exit', (code, signal) => {
|
||||
log.log(`Exit code: ${code} => ${signal}`)
|
||||
prefixedStdout?.unpipe(process.stdout)
|
||||
prefixedStderr?.unpipe(process.stderr)
|
||||
dfd.resolve(cp)
|
||||
})
|
||||
|
||||
cp.once('error', (e) => {
|
||||
log.error(`error executing ${command} ${writeError(e)}`)
|
||||
prefixedStdout?.unpipe(process.stdout)
|
||||
prefixedStderr?.unpipe(process.stderr)
|
||||
dfd.reject(e)
|
||||
})
|
||||
} else {
|
||||
cp.once('exit', (code, signal) => {
|
||||
log.log(`Exit code: ${code} => ${signal}`)
|
||||
})
|
||||
|
||||
cp.once('error', (e) => {
|
||||
log.error(`error executing ${command} ${writeError(e)}`)
|
||||
dfd.reject(e)
|
||||
})
|
||||
|
||||
if (waitForData) {
|
||||
cp.stdout?.once('data', () => {
|
||||
dfd.resolve(cp)
|
||||
})
|
||||
} else {
|
||||
dfd.resolve(cp)
|
||||
}
|
||||
}
|
||||
|
||||
return dfd.promise
|
||||
}
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import chalk from 'chalk'
|
||||
import { Transform } from 'stream'
|
||||
|
||||
export function prefixLog (prefixStr: string): Pick<typeof console, 'log' | 'error'> {
|
||||
const prefix = `[${chalk.gray(prefixStr)}]: `
|
||||
|
||||
return {
|
||||
log: (...args: string[]) => {
|
||||
return console.log(prefix, ...args)
|
||||
},
|
||||
error: (...args: string[]) => {
|
||||
return console.error(prefix, ...args.map((a) => chalk.red(a)))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a stream and prefixes with a given string
|
||||
* @param prefixStr
|
||||
|
||||
@@ -1,9 +1 @@
|
||||
require('@packages/server')
|
||||
|
||||
if (process.env.CYPRESS_INTERNAL_DEV_WATCH) {
|
||||
process.on('message', (msg) => {
|
||||
if (msg === 'close') {
|
||||
process.exit(0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15061,7 +15061,7 @@ clipboardy@1.2.3:
|
||||
arch "^2.1.0"
|
||||
execa "^0.8.0"
|
||||
|
||||
clipboardy@^2.3.0:
|
||||
clipboardy@2.3.0, clipboardy@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-2.3.0.tgz#3c2903650c68e46a91b388985bc2774287dba290"
|
||||
integrity sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==
|
||||
@@ -21507,7 +21507,7 @@ get-value@^2.0.3, get-value@^2.0.6:
|
||||
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
|
||||
integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
|
||||
|
||||
getenv@^1.0.0:
|
||||
getenv@1.0.0, getenv@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/getenv/-/getenv-1.0.0.tgz#874f2e7544fbca53c7a4738f37de8605c3fcfc31"
|
||||
integrity sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==
|
||||
@@ -36686,7 +36686,7 @@ serve-handler@6.1.2:
|
||||
path-to-regexp "2.2.1"
|
||||
range-parser "1.2.0"
|
||||
|
||||
serve-handler@^6.1.3:
|
||||
serve-handler@6.1.3, serve-handler@^6.1.3:
|
||||
version "6.1.3"
|
||||
resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.3.tgz#1bf8c5ae138712af55c758477533b9117f6435e8"
|
||||
integrity sha512-FosMqFBNrLyeiIDvP1zgO6YoTzFYHxLDEIavhlmQ+knB2Z7l1t+kGLHkZIDN7UVWqQAmKI3D20A6F6jo3nDd4w==
|
||||
@@ -36738,6 +36738,21 @@ serve@11.3.0:
|
||||
serve-handler "6.1.2"
|
||||
update-check "1.5.2"
|
||||
|
||||
serve@12.0.1:
|
||||
version "12.0.1"
|
||||
resolved "https://registry.yarnpkg.com/serve/-/serve-12.0.1.tgz#5b0e05849f5ed9b8aab0f30a298c3664bba052bb"
|
||||
integrity sha512-CQ4ikLpxg/wmNM7yivulpS6fhjRiFG6OjmP8ty3/c1SBnSk23fpKmLAV4HboTA2KrZhkUPlDfjDhnRmAjQ5Phw==
|
||||
dependencies:
|
||||
"@zeit/schemas" "2.6.0"
|
||||
ajv "6.12.6"
|
||||
arg "2.0.0"
|
||||
boxen "1.3.0"
|
||||
chalk "2.4.1"
|
||||
clipboardy "2.3.0"
|
||||
compression "1.7.3"
|
||||
serve-handler "6.1.3"
|
||||
update-check "1.5.2"
|
||||
|
||||
set-blocking@^2.0.0, set-blocking@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
|
||||
Reference in New Issue
Block a user