feat: improved DX for unified-desktop-gui (#18099)

- Moves graphql-codegen config to the root, which will serve all packages needing it
- Adds gulpfile for coordinating scripts related to dev environment in launchpad app
- yarn dev from the root runs yarn gulp dev, which:
  Runs autobarrel for rolling up the @packages/graphql files
  Cleans the dist & cache for .vite
  Starts the a codegen watcher for Nexus
  Starts the graphql-codegen --watch & highlights output
  Starts vite servers for launchpad & app
  Starts electron watch.js
This commit is contained in:
Tim Griesser
2021-09-15 11:54:14 -04:00
committed by GitHub
parent 8bda0a9d02
commit a851d797a8
54 changed files with 1258 additions and 426 deletions
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "../.eslintrc.json",
"rules": {
"no-console": "off"
}
}
+10
View File
@@ -0,0 +1,10 @@
import getenv from 'getenv'
// Where to fetch the remote "federated" schema TODO: add w/ stitching PR
// export const CYPRESS_INTERNAL_CLOUD_ENV = 'production'
export const CYPRESS_INTERNAL_DEBUG_PORT_STARTUP = getenv.int('CYPRESS_INTERNAL_DEBUG_PORT_STARTUP', 7200)
export const CYPRESS_INTERNAL_DEBUG_PORT_ELECTRON = getenv.int('CYPRESS_INTERNAL_DEBUG_PORT_ELECTRON', 7201)
export const CYPRESS_INTERNAL_DEBUG_PORT_CODEGEN = getenv.int('CYPRESS_INTERNAL_DEBUG_PORT_CODEGEN', 7202)
+69
View File
@@ -0,0 +1,69 @@
import gulp from 'gulp'
import { autobarrelWatcher } from './tasks/gulpAutobarrel'
import { startCypressWatch } from './tasks/gulpCypress'
import { graphqlCodegen, graphqlCodegenWatch, nexusCodegen, nexusCodegenWatch } from './tasks/gulpGraphql'
import { viteApp, viteCleanApp, viteCleanLaunchpad, viteLaunchpad } from './tasks/gulpVite'
import { makePathMap } from './utils/makePathMap'
gulp.task(
'dev',
gulp.series(
// Autobarrel watcher
autobarrelWatcher,
// Fetch the latest "remote" schema from the Cypress cloud
// TODO: with stitching bracnh
// fetchCloudSchema,
gulp.parallel(
// Clean the vite apps
viteCleanApp,
viteCleanLaunchpad,
),
// Codegen for our GraphQL Server so we have the latest schema to build the frontend codegen correctly
nexusCodegenWatch,
// ... and generate the correct GraphQL types for the frontend
graphqlCodegenWatch,
// 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
startCypressWatch,
),
)
gulp.task('buildProd', gulp.series(
nexusCodegen,
graphqlCodegen,
))
gulp.task(
'postinstall',
gulp.series(
gulp.parallel(
// Clean the vite apps
viteCleanApp,
viteCleanLaunchpad,
),
'buildProd',
),
)
// gulp.task(
// 'devLegacy', // Tim: TODO
// )
// gulp.task(
// 'debug', // Tim: TODO
// )
gulp.task(makePathMap)
gulp.task(nexusCodegen)
gulp.task(nexusCodegenWatch)
gulp.task(graphqlCodegen)
gulp.task(graphqlCodegenWatch)
+34
View File
@@ -0,0 +1,34 @@
/* eslint-disable */
// Auto-generated by makePathMap.ts
import path from 'path'
export const monorepoPaths = {
root: path.join(__dirname, '../..'),
pkgDir: path.join(__dirname, '../../packages'),
pkgApp: path.join(__dirname, '../../packages/app'),
pkgDesktopGui: path.join(__dirname, '../../packages/desktop-gui'),
pkgDriver: path.join(__dirname, '../../packages/driver'),
pkgElectron: path.join(__dirname, '../../packages/electron'),
pkgExample: path.join(__dirname, '../../packages/example'),
pkgExtension: path.join(__dirname, '../../packages/extension'),
pkgFrontendShared: path.join(__dirname, '../../packages/frontend-shared'),
pkgGraphql: path.join(__dirname, '../../packages/graphql'),
pkgHttpsProxy: path.join(__dirname, '../../packages/https-proxy'),
pkgLauncher: path.join(__dirname, '../../packages/launcher'),
pkgLaunchpad: path.join(__dirname, '../../packages/launchpad'),
pkgNetStubbing: path.join(__dirname, '../../packages/net-stubbing'),
pkgNetwork: path.join(__dirname, '../../packages/network'),
pkgProxy: path.join(__dirname, '../../packages/proxy'),
pkgReporter: path.join(__dirname, '../../packages/reporter'),
pkgResolveDist: path.join(__dirname, '../../packages/resolve-dist'),
pkgRewriter: path.join(__dirname, '../../packages/rewriter'),
pkgRoot: path.join(__dirname, '../../packages/root'),
pkgRunner: path.join(__dirname, '../../packages/runner'),
pkgRunnerCt: path.join(__dirname, '../../packages/runner-ct'),
pkgRunnerShared: path.join(__dirname, '../../packages/runner-shared'),
pkgServer: path.join(__dirname, '../../packages/server'),
pkgSocket: path.join(__dirname, '../../packages/socket'),
pkgTs: path.join(__dirname, '../../packages/ts'),
pkgTypes: path.join(__dirname, '../../packages/types'),
pkgUiComponents: path.join(__dirname, '../../packages/ui-components'),
pkgWebConfig: path.join(__dirname, '../../packages/web-config')
}
+17
View File
@@ -0,0 +1,17 @@
import { resolveAutobarrelConfig, autobarrelWatch } from 'autobarrel'
import path from 'path'
import { monorepoPaths } from '../monorepoPaths'
/**
* Creates "barrel" files according to config in autobarrel.json:
* https://github.com/tgriesser/autobarrel
* Particularly useful in @packages/graphql because we want to import all
* types into the root schema
*/
export async function autobarrelWatcher () {
await autobarrelWatch(
await resolveAutobarrelConfig({
path: path.join(monorepoPaths.root, 'autobarrel.json'),
}),
)
}
+89
View File
@@ -0,0 +1,89 @@
import chokidar from 'chokidar'
import path from 'path'
import childProcess, { ChildProcess } from 'child_process'
import pDefer from 'p-defer'
import { monorepoPaths } from '../monorepoPaths'
/**
* Starts cypress, but watches the GraphQL files & restarts the server
* when any of those change
*/
export function startCypressWatch () {
const watcher = chokidar.watch('src/**/*.{js,ts}', {
cwd: monorepoPaths.pkgGraphql,
ignored: /\.gen\.ts/,
ignoreInitial: true,
})
let child: ChildProcess | null = null
let isClosing = false
let isRestarting = false
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')
}
child = childProcess.fork(pathToCli, ['open', ...argv], {
stdio: 'inherit',
execArgv: [],
env: {
...process.env,
LAUNCHPAD: '1',
CYPRESS_INTERNAL_DEV_WATCH: 'true',
},
})
child.on('exit', (code) => {
if (isClosing) {
process.exit(code ?? 0)
}
})
child.on('disconnect', () => {
child = null
})
}
async function restartServer () {
if (isRestarting) {
return
}
const dfd = pDefer()
if (child) {
child.on('exit', dfd.resolve)
isRestarting = true
child.send('close')
} else {
dfd.resolve()
}
await dfd.promise
isRestarting = false
openServer()
}
watcher.on('add', restartServer)
watcher.on('change', restartServer)
openServer()
process.on('beforeExit', () => {
isClosing = true
child?.send('close')
})
}
+77
View File
@@ -0,0 +1,77 @@
import path from 'path'
import pDefer from 'p-defer'
import chalk from 'chalk'
import { nexusTypegen, watchNexusTypegen } from '../utils/nexusTypegenUtil'
import { monorepoPaths } from '../monorepoPaths'
import { spawned } from '../utils/childProcessUtils'
import { spawn } from 'child_process'
export async function nexusCodegen () {
return nexusTypegen({
cwd: monorepoPaths.pkgGraphql,
filePath: path.join(monorepoPaths.pkgGraphql, 'src/schema.ts'),
outputPath: path.join(monorepoPaths.pkgGraphql, 'src/gen/nxs.gen.ts'),
})
}
/**
* Watches & regenerates the
*/
export async function nexusCodegenWatch () {
return watchNexusTypegen({
cwd: monorepoPaths.pkgGraphql,
watchPaths: [
'src/**/*.ts',
],
filePath: path.join(monorepoPaths.pkgGraphql, 'src/schema.ts'),
outputPath: path.join(monorepoPaths.pkgGraphql, 'src/gen/nxs.gen.ts'),
})
}
export async function graphqlCodegen () {
return spawned('gql-codegen', 'yarn graphql-codegen --config graphql-codegen.yml', {
cwd: monorepoPaths.root,
waitForExit: true,
})
}
export async function graphqlCodegenWatch () {
const spawned = spawn('graphql-codegen', ['--watch', '--config', 'graphql-codegen.yml'], {
cwd: monorepoPaths.root,
})
const dfd = pDefer()
let hasResolved = false
spawned.stdout.on('data', (chunk) => {
const strs = `${chunk}`.split('\n').filter((f) => f)
const timestampRegex = /\[\d{2}:\d{2}:\d{2}\]/
const isFailureBlock = strs.some((s) => s.includes('[failed]'))
strs.forEach((str) => {
let codegenMsg = timestampRegex.test(str) ? str.slice(10) : str
if (codegenMsg.includes('Watching for changes') && !hasResolved) {
dfd.resolve({})
}
if (codegenMsg === str) {
process.stdout.write(
`${chalk.cyan('graphqlCodegen')}: ${chalk.gray(str)}\n`,
)
} else if (codegenMsg.startsWith(' Generate .') || isFailureBlock) {
codegenMsg = codegenMsg.includes('[failed]')
? chalk.red(codegenMsg)
: chalk.yellow(codegenMsg)
process.stdout.write(`${chalk.cyan('graphqlCodegen')}: ${codegenMsg}\n`)
}
})
})
spawned.stderr.on('data', (data) => {
console.error(chalk.red(String(data)))
})
return dfd.promise
}
+111
View File
@@ -0,0 +1,111 @@
import glob from 'glob'
import childProcess from 'child_process'
import util from 'util'
import path from 'path'
import chalk from 'chalk'
import { monorepoPaths } from '../monorepoPaths'
const execAsync = util.promisify(childProcess.exec)
process.on('unhandledRejection', (reason) => {
console.error(reason)
process.exit(1)
})
export function getTsPackages (packagesPath: string = ''): Promise<Set<string>> {
const dfd = {} as Record<string, any>
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve
dfd.reject = reject
})
glob(
path.join(monorepoPaths.root, packagesPath, '/packages/*/package.json'),
(err, packageJsons) => {
if (err) {
return dfd.reject(err)
}
type PackageJsonStructure = {
name: string
dependencies: Record<string, string>
devDependencies: Record<string, string>
scripts: Record<string, string>
}
const required = packageJsons.map((path) => {
return require(path)
}) as PackageJsonStructure[]
const packages = new Set<string>()
required.forEach((r) => {
// Select only packages that have build-ts
if (r.scripts && r.scripts['build-ts']) {
packages.add(r.name)
}
})
dfd.resolve(packages)
},
)
return dfd.promise
}
export async function buildPackageTsc ({
packagesPath = '',
tsPackages = new Set(),
onlyPackages,
}: {
packagesPath?: string
tsPackages?: Set<string>
onlyPackages?: string[]
}) {
console.log(
chalk.blue(`TSC: Building deps for ${onlyPackages || 'All Packages'}`),
)
const errors = []
let built = 0
const packages = onlyPackages || [...Array.from(tsPackages)]
for (const pkg of packages) {
try {
await execAsync('tsc', {
cwd: path.join(
__dirname,
'../../',
packagesPath,
`/packages/${pkg.replace(/\@(packages|cypress|frontend)\//, '')}`,
),
})
built++
console.log(
`${chalk.green(`Built`)} ${pkg} ${chalk.magenta(
`${built} / ${packages.length}`,
)}`,
)
} catch (e) {
console.log(
`${chalk.red(`Failed built`)} ${pkg} ${chalk.magenta(
`${built} / ${packages.length}`,
)}`,
)
errors.push({ package: pkg, stdout: e.stdout })
}
}
if (errors.length > 0) {
errors.forEach((e) => {
console.log(`Error building ${e.package}`)
console.error(chalk.red(e.stdout))
})
process.exit(1)
}
}
+21
View File
@@ -0,0 +1,21 @@
import detectPort from 'detect-port'
import { exit } from '../utils/exitUtil'
export async function friendlyStartupWarnings () {
const ALL_SERVER_PORTS_USED = [3000, 4000, 8484, 1234]
const unavaiablePorts: number[] = []
await Promise.all(
ALL_SERVER_PORTS_USED.map(async (port) => {
if (port !== (await detectPort(port))) {
unavaiablePorts.push(port)
}
}),
)
if (unavaiablePorts.length > 0) {
exit(
`The following ports needed by Cypress are already in use: ${unavaiablePorts.join(', ')}.\nCheck that you don't have another web-server running.\nThe command "pkill node -9" can be used to kill all node processes.`,
)
}
}
+55
View File
@@ -0,0 +1,55 @@
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'
const CYPRESS_VITE_APP_PORT = getenv.int('CYPRESS_VITE_APP_PORT', 3333)
const CYPRESS_VITE_LAUNCHPAD_PORT = getenv.int('CYPRESS_VITE_LAUNCHPAD_PORT', 3001)
export function viteApp () {
return viteDev('vite-app', `yarn vite --port ${CYPRESS_VITE_APP_PORT} --base /__vite__/`, {
cwd: monorepoPaths.pkgApp,
})
}
export function viteLaunchpad () {
return viteDev('vite-launchpad', `yarn vite --port ${CYPRESS_VITE_LAUNCHPAD_PORT}`, {
cwd: monorepoPaths.pkgLaunchpad,
})
}
export function viteCleanApp () {
return spawned('vite-clean', `yarn clean`, {
cwd: monorepoPaths.pkgApp,
})
}
export function viteCleanLaunchpad () {
return spawned('vite-clean', `yarn clean`, {
cwd: monorepoPaths.pkgLaunchpad,
})
}
function viteDev (
prefix: AllSpawnableApps,
command: string,
opts: SpawnOptions = {},
) {
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)
},
})
return dfd.promise
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"noEmit": true,
"strict": true,
"esModuleInterop": true,
"importsNotUsedAsValues": "error",
"types": []
},
"include": ["**/*"]
}
+136
View File
@@ -0,0 +1,136 @@
import { exec, ExecOptions, spawn, SpawnOptions } from 'child_process'
import through2 from 'through2'
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
}
export type AllSpawnableApps =
| `vite-${string}`
| `vite:build-${string}`
| 'gql-codegen'
interface SpawnedOptions extends SpawnOptions {
waitForExit?: boolean
}
export async function spawned (
prefix: AllSpawnableApps,
command: string,
opts: SpawnedOptions = {},
tapThrough: {
tapOut?: through2.TransformFunction
tapErr?: through2.TransformFunction
} = {},
) {
const { waitForExit, ...spawnOpts } = opts
spawningApps.add(prefix)
const [executable, ...rest] = command.split(' ')
const cp = spawn(executable, rest, {
stdio: 'pipe',
env: {
FORCE_COLOR: '1',
NODE_ENV: 'development',
...process.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}`))
prefixedStdout?.pipe(process.stdout)
prefixedStderr?.pipe(process.stderr)
// const cleanup = () => {
// prefixedStdout?.unpipe(process.stdout)
// prefixedStderr?.unpipe(process.stderr)
// }
// runningApps.set(prefix, [cp, [prefix, command, opts], cleanup])
return new Promise((resolve, reject) => {
if (waitForExit) {
cp.once('exit', () => {
resolve(cp)
})
cp.once('error', reject)
} else {
cp.stdout?.once('data', () => {
spawningApps.delete(prefix)
resolve(cp)
})
}
})
}
const execAsyncLocal = util.promisify(exec)
interface ExecAsyncOptions extends ExecOptions {
encoding?: string | null
silent?: boolean
}
export const execAsync = async (
command: string,
options: ExecAsyncOptions = {},
) => {
const { silent } = options
if (!silent) {
console.log(command)
}
const result = await execAsyncLocal(command, options)
if (!silent && typeof result.stdout === 'string' && result.stdout.length) {
console.log(result.stdout)
}
if (!silent && typeof result.stderr === 'string' && result.stderr.length) {
console.error(result.stderr)
}
return result
}
+13
View File
@@ -0,0 +1,13 @@
import chalk from 'chalk'
import dedent from 'dedent'
export function exit (msg: string | Error): void {
if (msg instanceof Error) {
console.log(msg.stack)
return exit(msg.message)
}
console.error(`\n${chalk.red(dedent(msg))}\n`)
process.exit(1)
}
+42
View File
@@ -0,0 +1,42 @@
import fs from 'fs-extra'
import path from 'path'
import _ from 'lodash'
const ROOT_DIR = path.join(__dirname, '../../..')
/**
* Builds
*/
export async function makePathMap () {
const packages = await fs.readdir(path.join(ROOT_DIR, 'packages'))
const dirs = await Promise.all(
packages.map(async (p) => {
try {
await fs.stat(path.join(ROOT_DIR, `packages/${p}/package.json`))
return p
} catch (e) {
return null
}
}),
)
await fs.writeFile(
path.join(__dirname, '../monorepoPaths.ts'),
`/* eslint-disable */
// Auto-generated by makePathMap.ts
import path from 'path'
export const monorepoPaths = {
root: path.join(__dirname, '../..'),
pkgDir: path.join(__dirname, '../../packages'),
${dirs
.filter((f) => f)
.map((dir) => {
return ` ${_.camelCase(
`pkg-${dir}`,
)}: path.join(__dirname, '../../packages/${dir}')`
}).join(',\n')}
}
`,
)
}
+90
View File
@@ -0,0 +1,90 @@
import { spawn, execSync } from 'child_process'
import chalk from 'chalk'
import pDefer from 'p-defer'
import chokidar from 'chokidar'
import _ from 'lodash'
interface NexusTypegenCfg {
cwd: string
/**
* Path to the file we need to execute to generate the schema
*/
filePath: string
outputPath?: string
}
function prefixTypegen (s: string) {
return `${chalk.cyan('nexusTypegen')}: ${s}`
}
export async function nexusTypegen (cfg: NexusTypegenCfg) {
const dfd = pDefer()
if (cfg.outputPath) {
execSync(`touch ${cfg.outputPath}`)
}
const out = spawn('node', ['-r', '@packages/ts/register', cfg.filePath], {
cwd: cfg.cwd,
env: {
...process.env,
CYPRESS_INTERNAL_NEXUS_CODEGEN: 'true',
},
})
out.stderr.on('data', (data) => {
process.stdout.write(prefixTypegen(chalk.red(String(data))))
dfd.resolve({})
})
out.stdout.on('data', (data) => {
const outString = String(data)
.split('\n')
.map((s) => prefixTypegen(chalk.magentaBright(s)))
.join('\n')
process.stdout.write('\n')
process.stdout.write(outString)
process.stdout.write('\n')
dfd.resolve({})
})
out.on('error', dfd.reject)
return dfd.promise
}
let debounced: Record<string, Function> = {}
const nexusTypegenDebounced = (cfg: NexusTypegenCfg) => {
debounced[cfg.filePath] =
debounced[cfg.filePath] ?? _.debounce(nexusTypegen, 500)
debounced[cfg.filePath](cfg)
}
interface NexusTypegenWatchCfg extends NexusTypegenCfg {
watchPaths: string[]
}
export async function watchNexusTypegen (cfg: NexusTypegenWatchCfg) {
const dfd = pDefer()
const watcher = chokidar.watch(cfg.watchPaths, {
cwd: cfg.cwd,
ignored: /\.gen\.ts/,
ignoreInitial: true,
})
watcher.on('all', (evt, path) => {
console.log(prefixTypegen(`${evt} ${path}`))
nexusTypegenDebounced(cfg)
})
watcher.on('ready', () => {
console.log(prefixTypegen(`Codegen Watcher Ready for ${cfg.filePath}`))
nexusTypegen(cfg).then(dfd.resolve, dfd.reject)
})
return dfd.promise
}
+56
View File
@@ -0,0 +1,56 @@
import chalk from 'chalk'
import { Transform } from 'stream'
/**
* Takes a stream and prefixes with a given string
* @param prefixStr
* @returns
*/
export function prefixStream (prefixStr: string | (() => string)) {
const prefix =
typeof prefixStr === 'string'
? Buffer.from(`[${chalk.gray(prefixStr)}]: `)
: () => Buffer.from(`[${chalk.gray(prefixStr())}]: `)
// https://stackoverflow.com/a/45126242
class PrefixStream extends Transform {
_rest?: Buffer
_transform (chunk: Buffer, encoding: string, cb: CallableFunction) {
this._rest =
this._rest && this._rest.length
? Buffer.concat([this._rest, chunk])
: chunk
let index
while (this._rest && (index = this._rest.indexOf('\n')) !== -1) {
const line = this._rest.slice(0, ++index)
this._rest = this._rest.slice(index)
this.push(
Buffer.concat([
typeof prefix === 'function' ? prefix() : prefix,
line,
]),
)
}
cb()
}
_flush (cb: CallableFunction) {
if (this._rest && this._rest.length) {
cb(
null,
Buffer.concat([
typeof prefix === 'function' ? prefix() : prefix,
this._rest,
]),
)
}
}
}
return new PrefixStream()
}
+1 -2
View File
@@ -1,7 +1,6 @@
process.env.GRAPHQL_CODEGEN = 'true'
require('@packages/server')
if (process.argv.includes('--devWatch')) {
if (process.env.CYPRESS_INTERNAL_DEV_WATCH) {
process.on('message', (msg) => {
if (msg === 'close') {
process.exit(0)
-68
View File
@@ -1,68 +0,0 @@
const chokidar = require('chokidar')
const childProcess = require('child_process')
const path = require('path')
const pDefer = require('p-defer')
const watcher = chokidar.watch('packages/graphql/src/**/*.{js,ts}', {
cwd: path.join(__dirname, '..'),
ignored: '**/nxs.gen.ts',
ignoreInitial: true,
})
/**
* @type {childProcess.ChildProcess}
*/
let child
let isClosing = false
let isRestarting = false
function runServer () {
if (child) {
child.removeAllListeners()
}
child = childProcess.fork(path.join(__dirname, 'start.js'), [...process.argv, '--devWatch'], {
stdio: 'inherit',
})
child.on('exit', (code) => {
if (isClosing) {
process.exit(code)
}
})
child.on('disconnect', () => {
child = null
})
}
async function restartServer () {
if (isRestarting) {
return
}
const dfd = pDefer()
if (child) {
child.on('exit', dfd.resolve)
isRestarting = true
child.send('close')
} else {
dfd.resolve()
}
await dfd.promise
isRestarting = false
runServer()
}
watcher.on('add', restartServer)
watcher.on('change', restartServer)
runServer()
process.on('beforeExit', () => {
isClosing = true
child?.send('close')
})