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:
Jessica Sachs
2021-09-30 12:11:47 -04:00
committed by GitHub
parent 2f5e53d442
commit d39b1694aa
43 changed files with 1113 additions and 339 deletions
+53 -5
View File
@@ -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
View File
@@ -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 })
+6
View File
@@ -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')
+1 -1
View File
@@ -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 }
}
+2
View File
@@ -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"
+5 -3
View File
@@ -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')
})
})
+2 -2
View File
@@ -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
}
}
}
-10
View File
@@ -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
-1
View File
@@ -3,7 +3,6 @@
"baseUrl": "http://localhost:5555",
"viewportWidth": 800,
"viewportHeight": 850,
"video": false,
"retries": {
"runMode": 2,
"openMode": 0
+4
View File
@@ -0,0 +1,4 @@
// Unsure why the ImportMeta type isn't picking this up.
interface ImportMetaEnv {
VITE_CYPRESS_INTERNAL_GQL_PORT: string
}
+2 -1
View File
@@ -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`
+1 -1
View File
@@ -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.
![graphql](./gql.png)
+1 -1
View File
@@ -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"
+2
View File
@@ -3,3 +3,5 @@ export { graphqlSchema } from './schema'
export { execute, parse, print } from 'graphql'
export { remoteSchemaWrapped } from './stitching/remoteSchemaWrapped'
//
+3 -3
View File
@@ -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',
}
+1 -1
View File
@@ -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`.
![graphql](../graphql/gql.png)
+3 -2
View File
@@ -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
}
@@ -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 -21
View File
@@ -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
}
+6 -5
View File
@@ -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"
},
+20 -5
View File
@@ -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')}`
}
+8
View File
@@ -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()
+18 -11
View File
@@ -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])
+5 -1
View File
@@ -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)
+33 -8
View File
@@ -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
View File
@@ -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
}
})
+146 -52
View File
@@ -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')
})
}
+3 -3
View File
@@ -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'))
+80
View File
@@ -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
View File
@@ -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
}
+201 -99
View File
@@ -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
}
+13
View File
@@ -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
-8
View File
@@ -1,9 +1 @@
require('@packages/server')
if (process.env.CYPRESS_INTERNAL_DEV_WATCH) {
process.on('message', (msg) => {
if (msg === 'close') {
process.exit(0)
}
})
}
+18 -3
View File
@@ -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"