dev playground: Support auto port assignment + refactor
This commit is contained in:
parent
8f0f76df54
commit
4d4d970278
1 changed files with 170 additions and 81 deletions
|
@ -3,7 +3,7 @@ import {writeFile, readFile} from 'fs/promises'
|
|||
import path from 'path'
|
||||
import type {BuildOptions} from 'esbuild'
|
||||
import esbuild from 'esbuild'
|
||||
import type {ServerResponse} from 'http'
|
||||
import type {IncomingMessage, ServerResponse} from 'http'
|
||||
import {definedGlobals} from '../../../theatre/devEnv/buildUtils'
|
||||
import {mapValues} from 'lodash-es'
|
||||
import {createServer, request} from 'http'
|
||||
|
@ -11,17 +11,57 @@ import {spawn} from 'child_process'
|
|||
import React from 'react'
|
||||
import {renderToStaticMarkup} from 'react-dom/server'
|
||||
import {Home} from './Home'
|
||||
import type {Server} from 'net'
|
||||
|
||||
const playgroundDir = path.join(__dirname, '..')
|
||||
const buildDir = path.join(playgroundDir, 'build')
|
||||
const sharedDir = path.join(playgroundDir, 'src/shared')
|
||||
const personalDir = path.join(playgroundDir, 'src/personal')
|
||||
const testDir = path.join(playgroundDir, 'src/tests')
|
||||
const playgroundDir = (folder: string) => path.join(__dirname, '..', folder)
|
||||
const buildDir = playgroundDir('build')
|
||||
const sharedDir = playgroundDir('src/shared')
|
||||
const personalDir = playgroundDir('src/personal')
|
||||
const testDir = playgroundDir('src/tests')
|
||||
|
||||
const dev = /^--dev|-d$/.test(process.argv[process.argv.length - 1])
|
||||
const port = 8080
|
||||
const dev = process.argv.find((arg) => ['--dev', '-d'].includes(arg)) != null
|
||||
const defaultPort = 8080
|
||||
|
||||
const clients: ServerResponse[] = []
|
||||
const liveReload =
|
||||
(dev || undefined) &&
|
||||
((): {
|
||||
handleRequest(req: IncomingMessage, res: ServerResponse): boolean
|
||||
hasOpenConnections(): boolean
|
||||
esbuildBanner: esbuild.BuildOptions['banner']
|
||||
esbuildWatch: esbuild.WatchMode
|
||||
} => {
|
||||
const openResponses: ServerResponse[] = []
|
||||
return {
|
||||
handleRequest(req, res) {
|
||||
// If special /esbuild url requested, subscribe clients to changes
|
||||
if (req.url === '/esbuild') {
|
||||
openResponses.push(
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
}),
|
||||
)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
hasOpenConnections() {
|
||||
return openResponses.length > 0
|
||||
},
|
||||
esbuildBanner: {
|
||||
js: ' (() => new EventSource("/esbuild").onmessage = () => location.reload())();',
|
||||
},
|
||||
esbuildWatch: {
|
||||
onRebuild(error) {
|
||||
// Notify clients on rebuild
|
||||
openResponses.forEach((res) => res.write('data: update\n\n'))
|
||||
openResponses.length = 0
|
||||
console.error(error ? error : 'Reloading...')
|
||||
},
|
||||
},
|
||||
}
|
||||
})()
|
||||
|
||||
type Groups = {
|
||||
[group: string]: {
|
||||
|
@ -40,11 +80,15 @@ const groups: Groups = Object.fromEntries(
|
|||
return [
|
||||
path.basename(groupDir),
|
||||
Object.fromEntries(
|
||||
readdirSync(groupDir).map((moduleDir) => [
|
||||
path.basename(moduleDir),
|
||||
readdirSync(groupDir).map((moduleDirName) => [
|
||||
path.basename(moduleDirName),
|
||||
{
|
||||
entryDir: path.join(groupDir, moduleDir),
|
||||
outDir: path.join(buildDir, path.basename(groupDir), moduleDir),
|
||||
entryDir: path.join(groupDir, moduleDirName),
|
||||
outDir: path.join(
|
||||
buildDir,
|
||||
path.basename(groupDir),
|
||||
moduleDirName,
|
||||
),
|
||||
},
|
||||
]),
|
||||
),
|
||||
|
@ -79,7 +123,7 @@ const config: BuildOptions = {
|
|||
entryPoints,
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
outdir: path.join(playgroundDir, 'build'),
|
||||
outdir: playgroundDir('build'),
|
||||
target: ['firefox88'],
|
||||
loader: {
|
||||
'.png': 'file',
|
||||
|
@ -91,19 +135,8 @@ const config: BuildOptions = {
|
|||
...definedGlobals,
|
||||
'window.__IS_VISUAL_REGRESSION_TESTING': 'true',
|
||||
},
|
||||
banner: dev
|
||||
? {
|
||||
js: ' (() => new EventSource("/esbuild").onmessage = () => location.reload())();',
|
||||
}
|
||||
: undefined,
|
||||
watch: dev && {
|
||||
onRebuild(error) {
|
||||
// Notify clients on rebuild
|
||||
clients.forEach((res) => res.write('data: update\n\n'))
|
||||
clients.length = 0
|
||||
console.log(error ? error : 'Reloading...')
|
||||
},
|
||||
},
|
||||
banner: liveReload?.esbuildBanner,
|
||||
watch: liveReload?.esbuildWatch,
|
||||
}
|
||||
|
||||
esbuild
|
||||
|
@ -134,7 +167,7 @@ esbuild
|
|||
console.log(err)
|
||||
return process.exit(1)
|
||||
})
|
||||
.then(() => {
|
||||
.then(async () => {
|
||||
// Only start dev server in dev, otherwise just run build and that's it
|
||||
if (!dev) {
|
||||
return
|
||||
|
@ -142,58 +175,114 @@ esbuild
|
|||
|
||||
// We start ESBuild serve with no build config because it doesn't need to build
|
||||
// anything, we are already using ESBuild watch.
|
||||
esbuild
|
||||
.serve({servedir: path.join(playgroundDir, 'build')}, {})
|
||||
.then(({port: esbuildPort}) => {
|
||||
// Create proxy
|
||||
createServer((req, res) => {
|
||||
const {url, method, headers} = req
|
||||
// If special /esbuild url requested, subscribe clients to changes
|
||||
if (req.url === '/esbuild') {
|
||||
return clients.push(
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
}),
|
||||
)
|
||||
}
|
||||
// Otherwise forward requests to ESBuild server
|
||||
req.pipe(
|
||||
request(
|
||||
{
|
||||
hostname: '0.0.0.0',
|
||||
port: esbuildPort,
|
||||
path: url,
|
||||
method,
|
||||
headers,
|
||||
},
|
||||
(prxRes) => {
|
||||
res.writeHead(prxRes.statusCode!, prxRes.headers)
|
||||
prxRes.pipe(res, {end: true})
|
||||
},
|
||||
),
|
||||
{end: true},
|
||||
)
|
||||
}).listen(port, () => {
|
||||
console.log('Playground running at', 'http://localhost:' + port)
|
||||
})
|
||||
const {port: esbuildPort} = await esbuild.serve(
|
||||
{servedir: playgroundDir('build')},
|
||||
{},
|
||||
)
|
||||
|
||||
// If not in CI, try to spawn a browser
|
||||
if (!process.env.CI) {
|
||||
setTimeout(() => {
|
||||
const open = {
|
||||
darwin: ['open'],
|
||||
linux: ['xdg-open'],
|
||||
win32: ['cmd', '/c', 'start'],
|
||||
}
|
||||
const platform = process.platform as keyof typeof open
|
||||
if (clients.length === 0)
|
||||
spawn(open[platform][0], [
|
||||
...open[platform].slice(1),
|
||||
`http://localhost:${port}`,
|
||||
])
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
const proxyServer = createServer((req, res) => {
|
||||
const {url, method, headers} = req
|
||||
if (liveReload?.handleRequest(req, res)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise forward requests to ESBuild server
|
||||
req.pipe(
|
||||
request(
|
||||
{
|
||||
hostname: '0.0.0.0',
|
||||
port: esbuildPort,
|
||||
path: url,
|
||||
method,
|
||||
headers,
|
||||
},
|
||||
(prxRes) => {
|
||||
res.writeHead(prxRes.statusCode!, prxRes.headers)
|
||||
prxRes.pipe(res, {end: true})
|
||||
},
|
||||
),
|
||||
{end: true},
|
||||
)
|
||||
})
|
||||
|
||||
const isCI = process.env.CI
|
||||
const portTries = isCI ? 1 : 10
|
||||
const portChosen = await tryMultiplePorts(
|
||||
defaultPort,
|
||||
portTries,
|
||||
proxyServer,
|
||||
)
|
||||
|
||||
const hostedAt = `http://localhost:${portChosen}`
|
||||
|
||||
console.log('Playground running at', hostedAt)
|
||||
|
||||
// If not in CI, try to spawn a browser
|
||||
if (!isCI) {
|
||||
setTimeout(() => {
|
||||
if (!liveReload?.hasOpenConnections()) openForOS(hostedAt)
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
|
||||
function openForOS(hostedAt: string) {
|
||||
const open = {
|
||||
darwin: ['open'],
|
||||
linux: ['xdg-open'],
|
||||
win32: ['cmd', '/c', 'start'],
|
||||
}
|
||||
const platform = process.platform as keyof typeof open
|
||||
if (open[platform]) {
|
||||
spawn(open[platform][0], [...open[platform].slice(1), hostedAt])
|
||||
} else {
|
||||
console.error(
|
||||
`Failed to open (${hostedAt}) for unconfigured platform (${platform})`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function tryMultiplePorts(
|
||||
port: number,
|
||||
tries: number,
|
||||
server: Server,
|
||||
): Promise<number> {
|
||||
let portToTry = port
|
||||
let firstError = null
|
||||
let lastError = null
|
||||
|
||||
while (portToTry < port + tries) {
|
||||
try {
|
||||
await new Promise((res, rej) => {
|
||||
const onListening = () => (rm(), res(true))
|
||||
const onError = () => (rm(), rej())
|
||||
const rm = () => {
|
||||
server.off('error', onError)
|
||||
server.off('listening', onListening)
|
||||
}
|
||||
|
||||
server
|
||||
.listen(portToTry)
|
||||
.on('listening', onListening)
|
||||
.on('error', onError)
|
||||
})
|
||||
|
||||
firstError = null
|
||||
lastError = null
|
||||
break // found a working port
|
||||
} catch (err) {
|
||||
if (!firstError) firstError = err
|
||||
lastError = err
|
||||
portToTry += 1
|
||||
}
|
||||
}
|
||||
|
||||
if (firstError) {
|
||||
console.error(firstError)
|
||||
console.error(lastError)
|
||||
throw new Error(
|
||||
`Failed to find port starting at ${port} with ${tries} tries.`,
|
||||
)
|
||||
}
|
||||
|
||||
return portToTry
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue