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 path from 'path'
|
||||||
import type {BuildOptions} from 'esbuild'
|
import type {BuildOptions} from 'esbuild'
|
||||||
import esbuild from 'esbuild'
|
import esbuild from 'esbuild'
|
||||||
import type {ServerResponse} from 'http'
|
import type {IncomingMessage, ServerResponse} from 'http'
|
||||||
import {definedGlobals} from '../../../theatre/devEnv/buildUtils'
|
import {definedGlobals} from '../../../theatre/devEnv/buildUtils'
|
||||||
import {mapValues} from 'lodash-es'
|
import {mapValues} from 'lodash-es'
|
||||||
import {createServer, request} from 'http'
|
import {createServer, request} from 'http'
|
||||||
|
@ -11,17 +11,57 @@ import {spawn} from 'child_process'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {renderToStaticMarkup} from 'react-dom/server'
|
import {renderToStaticMarkup} from 'react-dom/server'
|
||||||
import {Home} from './Home'
|
import {Home} from './Home'
|
||||||
|
import type {Server} from 'net'
|
||||||
|
|
||||||
const playgroundDir = path.join(__dirname, '..')
|
const playgroundDir = (folder: string) => path.join(__dirname, '..', folder)
|
||||||
const buildDir = path.join(playgroundDir, 'build')
|
const buildDir = playgroundDir('build')
|
||||||
const sharedDir = path.join(playgroundDir, 'src/shared')
|
const sharedDir = playgroundDir('src/shared')
|
||||||
const personalDir = path.join(playgroundDir, 'src/personal')
|
const personalDir = playgroundDir('src/personal')
|
||||||
const testDir = path.join(playgroundDir, 'src/tests')
|
const testDir = playgroundDir('src/tests')
|
||||||
|
|
||||||
const dev = /^--dev|-d$/.test(process.argv[process.argv.length - 1])
|
const dev = process.argv.find((arg) => ['--dev', '-d'].includes(arg)) != null
|
||||||
const port = 8080
|
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 = {
|
type Groups = {
|
||||||
[group: string]: {
|
[group: string]: {
|
||||||
|
@ -40,11 +80,15 @@ const groups: Groups = Object.fromEntries(
|
||||||
return [
|
return [
|
||||||
path.basename(groupDir),
|
path.basename(groupDir),
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
readdirSync(groupDir).map((moduleDir) => [
|
readdirSync(groupDir).map((moduleDirName) => [
|
||||||
path.basename(moduleDir),
|
path.basename(moduleDirName),
|
||||||
{
|
{
|
||||||
entryDir: path.join(groupDir, moduleDir),
|
entryDir: path.join(groupDir, moduleDirName),
|
||||||
outDir: path.join(buildDir, path.basename(groupDir), moduleDir),
|
outDir: path.join(
|
||||||
|
buildDir,
|
||||||
|
path.basename(groupDir),
|
||||||
|
moduleDirName,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
@ -79,7 +123,7 @@ const config: BuildOptions = {
|
||||||
entryPoints,
|
entryPoints,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
outdir: path.join(playgroundDir, 'build'),
|
outdir: playgroundDir('build'),
|
||||||
target: ['firefox88'],
|
target: ['firefox88'],
|
||||||
loader: {
|
loader: {
|
||||||
'.png': 'file',
|
'.png': 'file',
|
||||||
|
@ -91,19 +135,8 @@ const config: BuildOptions = {
|
||||||
...definedGlobals,
|
...definedGlobals,
|
||||||
'window.__IS_VISUAL_REGRESSION_TESTING': 'true',
|
'window.__IS_VISUAL_REGRESSION_TESTING': 'true',
|
||||||
},
|
},
|
||||||
banner: dev
|
banner: liveReload?.esbuildBanner,
|
||||||
? {
|
watch: liveReload?.esbuildWatch,
|
||||||
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...')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
esbuild
|
esbuild
|
||||||
|
@ -134,7 +167,7 @@ esbuild
|
||||||
console.log(err)
|
console.log(err)
|
||||||
return process.exit(1)
|
return process.exit(1)
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(async () => {
|
||||||
// Only start dev server in dev, otherwise just run build and that's it
|
// Only start dev server in dev, otherwise just run build and that's it
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
return
|
return
|
||||||
|
@ -142,58 +175,114 @@ esbuild
|
||||||
|
|
||||||
// We start ESBuild serve with no build config because it doesn't need to build
|
// We start ESBuild serve with no build config because it doesn't need to build
|
||||||
// anything, we are already using ESBuild watch.
|
// anything, we are already using ESBuild watch.
|
||||||
esbuild
|
const {port: esbuildPort} = await esbuild.serve(
|
||||||
.serve({servedir: path.join(playgroundDir, 'build')}, {})
|
{servedir: 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)
|
|
||||||
})
|
|
||||||
|
|
||||||
// If not in CI, try to spawn a browser
|
const proxyServer = createServer((req, res) => {
|
||||||
if (!process.env.CI) {
|
const {url, method, headers} = req
|
||||||
setTimeout(() => {
|
if (liveReload?.handleRequest(req, res)) {
|
||||||
const open = {
|
return
|
||||||
darwin: ['open'],
|
}
|
||||||
linux: ['xdg-open'],
|
|
||||||
win32: ['cmd', '/c', 'start'],
|
// Otherwise forward requests to ESBuild server
|
||||||
}
|
req.pipe(
|
||||||
const platform = process.platform as keyof typeof open
|
request(
|
||||||
if (clients.length === 0)
|
{
|
||||||
spawn(open[platform][0], [
|
hostname: '0.0.0.0',
|
||||||
...open[platform].slice(1),
|
port: esbuildPort,
|
||||||
`http://localhost:${port}`,
|
path: url,
|
||||||
])
|
method,
|
||||||
}, 1000)
|
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