Simplify playground/devEnv (#397
This commit is contained in:
parent
93fc53c45c
commit
c75f998174
8 changed files with 402 additions and 488 deletions
|
@ -9,10 +9,8 @@ import {ServerStyleSheet} from 'styled-components'
|
|||
import {definedGlobals} from '../../../theatre/devEnv/definedGlobals'
|
||||
import {createEsbuildLiveReloadTools} from './createEsbuildLiveReloadTools'
|
||||
import {createProxyServer} from './createProxyServer'
|
||||
import {createServerForceClose} from './createServerForceClose'
|
||||
import {PlaygroundPage} from './home/PlaygroundPage'
|
||||
import {openForOS} from './openForOS'
|
||||
import {timer} from './timer'
|
||||
import {tryMultiplePorts} from './tryMultiplePorts'
|
||||
|
||||
const playgroundDir = (folder: string) => path.join(__dirname, '..', folder)
|
||||
|
@ -22,7 +20,7 @@ const sharedDir = playgroundDir('src/shared')
|
|||
const personalDir = playgroundDir('src/personal')
|
||||
const testDir = playgroundDir('src/tests')
|
||||
|
||||
export async function start(options: {
|
||||
async function start(options: {
|
||||
/** enable live reload and watching stuff */
|
||||
dev: boolean
|
||||
/** make some UI elements predictable by setting the __IS_VISUAL_REGRESSION_TESTING value on window */
|
||||
|
@ -30,11 +28,10 @@ export async function start(options: {
|
|||
serve?: {
|
||||
findAvailablePort: boolean
|
||||
openBrowser: boolean
|
||||
waitBeforeStartingServer?: Promise<void>
|
||||
/** defaults to 8080 */
|
||||
defaultPort?: number
|
||||
}
|
||||
}): Promise<{stop(): Promise<void>}> {
|
||||
}): Promise<void> {
|
||||
const defaultPort = options.serve?.defaultPort ?? 8080
|
||||
|
||||
const liveReload =
|
||||
|
@ -54,71 +51,65 @@ export async function start(options: {
|
|||
|
||||
// Collect all entry directories per module per group
|
||||
const groups: Groups = await Promise.all(
|
||||
[sharedDir, personalDir, testDir].map(async (groupDir) =>
|
||||
readdir(groupDir)
|
||||
.then(async (groupDirItems) => [
|
||||
path.basename(groupDir),
|
||||
await Promise.all(
|
||||
groupDirItems.map(
|
||||
async (
|
||||
[sharedDir, personalDir, testDir].map(async (groupDir) => {
|
||||
let groupDirItems: string[]
|
||||
|
||||
try {
|
||||
groupDirItems = await readdir(groupDir)
|
||||
} catch (error) {
|
||||
// If the group dir doesn't exist, we just set its entry to undefined
|
||||
return [path.basename(groupDir), undefined]
|
||||
}
|
||||
|
||||
const allEntries = await Promise.all(
|
||||
groupDirItems.map(
|
||||
async (
|
||||
moduleDirName,
|
||||
): Promise<[string, PlaygroundExample | undefined]> => {
|
||||
const playgroundKey = path.basename(moduleDirName)
|
||||
const entryFilePath = path.join(
|
||||
groupDir,
|
||||
moduleDirName,
|
||||
'index.tsx',
|
||||
)
|
||||
|
||||
if (
|
||||
!(await stat(entryFilePath)
|
||||
.then((s) => s.isFile())
|
||||
.catch(() => false))
|
||||
)
|
||||
return [playgroundKey, undefined]
|
||||
|
||||
const playgroundExample = {
|
||||
useHtml: await readFile(
|
||||
path.join(groupDir, moduleDirName, 'index.html'),
|
||||
'utf-8',
|
||||
).catch(() => undefined),
|
||||
entryFilePath,
|
||||
outDir: path.join(
|
||||
buildDir,
|
||||
path.basename(groupDir),
|
||||
moduleDirName,
|
||||
): Promise<[string, PlaygroundExample | undefined]> => {
|
||||
const entryKey = path.basename(moduleDirName)
|
||||
const entryFilePath = path.join(
|
||||
groupDir,
|
||||
moduleDirName,
|
||||
'index.tsx',
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
if (
|
||||
!(await stat(entryFilePath)
|
||||
.then((s) => s.isFile())
|
||||
.catch(() => false))
|
||||
)
|
||||
return [entryKey, undefined]
|
||||
|
||||
return [
|
||||
entryKey,
|
||||
{
|
||||
// Including your own html file for playground is an experimental feature,
|
||||
// it's not quite ready for "prime time" and advertising to the masses until
|
||||
// it properly handles file watching.
|
||||
// It's good for now, since we can use it for some demos, just make sure that
|
||||
// you add a comment to the custom index.html file saying that you have to
|
||||
// restart playground server entirely to see changes.
|
||||
useHtml: await readFile(
|
||||
path.join(groupDir, moduleDirName, 'index.html'),
|
||||
'utf-8',
|
||||
).catch(() => undefined),
|
||||
entryFilePath,
|
||||
outDir: path.join(
|
||||
buildDir,
|
||||
path.basename(groupDir),
|
||||
moduleDirName,
|
||||
),
|
||||
},
|
||||
]
|
||||
},
|
||||
),
|
||||
).then((entries) =>
|
||||
Object.fromEntries(
|
||||
entries.filter((entry) => entry[1] !== undefined),
|
||||
),
|
||||
),
|
||||
])
|
||||
.catch(() =>
|
||||
// If the group dir doesn't exist, we just set its entry to undefined
|
||||
[path.basename(groupDir), undefined],
|
||||
return [playgroundKey, playgroundExample]
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const validEntries = allEntries.filter(
|
||||
([_, playgroundExample]) => playgroundExample !== undefined,
|
||||
)
|
||||
|
||||
return [path.basename(groupDir), Object.fromEntries(validEntries)]
|
||||
}),
|
||||
).then((entries) =>
|
||||
Object.fromEntries(
|
||||
// and then filter it out.
|
||||
entries.filter((entry) => entry[1] !== undefined),
|
||||
),
|
||||
)
|
||||
.then((entries) =>
|
||||
Object.fromEntries(
|
||||
// and then filter it out.
|
||||
entries.filter((entry) => entry[1] !== undefined),
|
||||
),
|
||||
)
|
||||
.catch(wrapCatch('reading group dirs'))
|
||||
|
||||
// Collect all entry files
|
||||
const entryPoints = Object.values(groups)
|
||||
|
@ -126,8 +117,8 @@ export async function start(options: {
|
|||
.map((module) => module.entryFilePath)
|
||||
|
||||
// Collect all output directories
|
||||
const outModules = Object.values(groups).flatMap((group) =>
|
||||
Object.values(group),
|
||||
const outModules: PlaygroundExample[] = Object.values(groups).flatMap(
|
||||
(group) => Object.values(group),
|
||||
)
|
||||
|
||||
// Render home page contents
|
||||
|
@ -155,8 +146,6 @@ export async function start(options: {
|
|||
}
|
||||
})()
|
||||
|
||||
const _initialBuild = timer('esbuild initial playground entry point builds')
|
||||
|
||||
const esbuildConfig: BuildOptions = {
|
||||
entryPoints,
|
||||
bundle: true,
|
||||
|
@ -179,12 +168,12 @@ export async function start(options: {
|
|||
'process.env.BUILT_FOR_PLAYGROUND': JSON.stringify('true'),
|
||||
},
|
||||
banner: liveReload?.esbuildBanner,
|
||||
watch: liveReload?.esbuildWatch && {
|
||||
onRebuild(error, result) {
|
||||
esbuildWatchStop = result?.stop ?? esbuildWatchStop
|
||||
liveReload?.esbuildWatch.onRebuild?.(error, result)
|
||||
},
|
||||
},
|
||||
// watch: liveReload?.esbuildWatch && {
|
||||
// onRebuild(error, result) {
|
||||
// esbuildWatchStop = result?.stop ?? esbuildWatchStop
|
||||
// liveReload?.esbuildWatch.onRebuild?.(error, result)
|
||||
// },
|
||||
// },
|
||||
plugins: [
|
||||
{
|
||||
name: 'watch playground assets',
|
||||
|
@ -234,87 +223,70 @@ export async function start(options: {
|
|||
],
|
||||
}
|
||||
|
||||
let esbuildWatchStop: undefined | (() => void)
|
||||
const ctx = await esbuild.context(esbuildConfig)
|
||||
|
||||
await esbuild
|
||||
.build(esbuildConfig)
|
||||
.finally(() => _initialBuild.stop())
|
||||
.catch(
|
||||
// if in dev mode, permit continuing to watch even if there was an error
|
||||
options.dev
|
||||
? () => Promise.resolve()
|
||||
: wrapCatch(`failed initial esbuild.build`),
|
||||
)
|
||||
.then(async (buildResult) => {
|
||||
esbuildWatchStop = buildResult?.stop
|
||||
// Read index.html template
|
||||
const index = await readFile(
|
||||
path.join(__dirname, 'index.html'),
|
||||
'utf8',
|
||||
).catch(wrapCatch('reading index.html template'))
|
||||
await Promise.all([
|
||||
// Write home page
|
||||
writeFile(
|
||||
path.join(buildDir, 'index.html'),
|
||||
index
|
||||
.replace(/<\/head>/, `${homeHtml.head}<\/head>`)
|
||||
.replace(/<body>/, `<body>${homeHtml.html}`),
|
||||
'utf-8',
|
||||
).catch(wrapCatch('writing build index.html')),
|
||||
// Write module pages
|
||||
...outModules.map((outModule) =>
|
||||
writeFile(
|
||||
path.join(outModule.outDir, 'index.html'),
|
||||
// Insert the script
|
||||
(outModule.useHtml ?? index).replace(
|
||||
/<\/body>/,
|
||||
`<script src="${path.join(
|
||||
'/',
|
||||
path.relative(buildDir, outModule.outDir),
|
||||
'index.js',
|
||||
)}"></script></body>`,
|
||||
),
|
||||
'utf-8',
|
||||
).catch(
|
||||
wrapCatch(
|
||||
`writing index.html for ${path.relative(
|
||||
buildDir,
|
||||
outModule.outDir,
|
||||
)}`,
|
||||
),
|
||||
),
|
||||
if (liveReload) {
|
||||
await ctx.watch()
|
||||
} else {
|
||||
await ctx.rebuild()
|
||||
}
|
||||
|
||||
// Read index.html template
|
||||
const index = await readFile(
|
||||
path.join(__dirname, 'index.html'),
|
||||
'utf8',
|
||||
).catch(wrapCatch('reading index.html template'))
|
||||
|
||||
await Promise.all([
|
||||
// Write home page
|
||||
writeFile(
|
||||
path.join(buildDir, 'index.html'),
|
||||
index
|
||||
.replace(/<\/head>/, `${homeHtml.head}<\/head>`)
|
||||
.replace(/<body>/, `<body>${homeHtml.html}`),
|
||||
'utf-8',
|
||||
).catch(wrapCatch('writing build index.html')),
|
||||
// Write module pages
|
||||
...outModules.map((outModule) =>
|
||||
writeFile(
|
||||
path.join(outModule.outDir, 'index.html'),
|
||||
// Insert the script
|
||||
(outModule.useHtml ?? index).replace(
|
||||
/<\/body>/,
|
||||
`<script src="${path.join(
|
||||
'/',
|
||||
path.relative(buildDir, outModule.outDir),
|
||||
'index.js',
|
||||
)}"></script></body>`,
|
||||
),
|
||||
])
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('build.ts: esbuild or html files writing error', err)
|
||||
return process.exit(1)
|
||||
})
|
||||
'utf-8',
|
||||
).catch(
|
||||
wrapCatch(
|
||||
`writing index.html for ${path.relative(buildDir, outModule.outDir)}`,
|
||||
),
|
||||
),
|
||||
),
|
||||
])
|
||||
|
||||
// Only start dev server in serve, otherwise just run build and that's it
|
||||
if (!options.serve) {
|
||||
return {
|
||||
stop() {
|
||||
esbuildWatchStop?.()
|
||||
return Promise.resolve()
|
||||
},
|
||||
}
|
||||
await ctx.dispose()
|
||||
return
|
||||
}
|
||||
|
||||
const {serve} = options
|
||||
await serve.waitBeforeStartingServer
|
||||
|
||||
// We start ESBuild serve with no build config because it doesn't need to build
|
||||
// anything, we are already using ESBuild watch.
|
||||
/** See https://esbuild.github.io/api/#serve-return-values */
|
||||
const esbuildServe = await esbuild.serve({servedir: buildDir}, {})
|
||||
const esbuildServe = await ctx.serve({servedir: buildDir})
|
||||
|
||||
const proxyServer = createProxyServer(liveReload?.handleRequest, {
|
||||
hostname: '0.0.0.0',
|
||||
port: esbuildServe.port,
|
||||
})
|
||||
|
||||
const proxyForceExit = createServerForceClose(proxyServer)
|
||||
// const proxyForceExit = createServerForceClose(proxyServer)
|
||||
const portTries = serve.findAvailablePort ? 10 : 1
|
||||
const portChosen = await tryMultiplePorts(defaultPort, portTries, proxyServer)
|
||||
|
||||
|
@ -328,15 +300,12 @@ export async function start(options: {
|
|||
}, 1000)
|
||||
}
|
||||
|
||||
return {
|
||||
stop() {
|
||||
esbuildServe.stop()
|
||||
esbuildWatchStop?.()
|
||||
return Promise.all([proxyForceExit(), esbuildServe.wait]).then(() => {
|
||||
// map to void for type defs
|
||||
})
|
||||
},
|
||||
}
|
||||
// return {
|
||||
// async stop() {
|
||||
// esbuildWatchStop?.()
|
||||
// await proxyForceExit()
|
||||
// },
|
||||
// }
|
||||
}
|
||||
|
||||
function wrapCatch(message: string) {
|
||||
|
@ -344,3 +313,27 @@ function wrapCatch(message: string) {
|
|||
return Promise.reject(`Rejected "${message}":\n ${err.toString()}`)
|
||||
}
|
||||
}
|
||||
|
||||
const dev = process.argv.find((arg) => ['--dev', '-d'].includes(arg)) != null
|
||||
|
||||
const serve =
|
||||
process.argv.find((arg) => ['--serve'].includes(arg)) != null || undefined
|
||||
|
||||
const isCI = Boolean(process.env.CI)
|
||||
|
||||
start({
|
||||
dev: !isCI && dev,
|
||||
isVisualRegressionTesting: isCI,
|
||||
serve: serve && {
|
||||
findAvailablePort: !isCI,
|
||||
// If not in CI, try to spawn a browser
|
||||
openBrowser: !isCI,
|
||||
// waitBeforeStartingServer: current?.stop(),
|
||||
},
|
||||
}).then(
|
||||
() => {},
|
||||
(err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
},
|
||||
)
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
// @ts-check
|
||||
const {timer} = require('./timer')
|
||||
|
||||
const dev = process.argv.find((arg) => ['--dev', '-d'].includes(arg)) != null
|
||||
const serve =
|
||||
process.argv.find((arg) => ['--serve'].includes(arg)) != null || undefined
|
||||
|
||||
const isCI = Boolean(process.env.CI)
|
||||
|
||||
/** Currently running server that can be stopped before restarting */
|
||||
let current
|
||||
|
||||
console.log('cli.js', {dev, serve, isCI})
|
||||
|
||||
function onUpdatedBuildScript(rebuild) {
|
||||
delete require.cache[require.resolve('./build.compiled')]
|
||||
/** @type {import("./build")} */
|
||||
const module = require('./build.compiled')
|
||||
const _start = timer('build.compiled start')
|
||||
try {
|
||||
module
|
||||
.start({
|
||||
dev: !isCI && dev,
|
||||
isVisualRegressionTesting: isCI,
|
||||
serve: serve && {
|
||||
findAvailablePort: !isCI,
|
||||
// If not in CI, try to spawn a browser
|
||||
openBrowser: !isCI && !rebuild,
|
||||
waitBeforeStartingServer: current?.stop(),
|
||||
},
|
||||
})
|
||||
.then((running) => {
|
||||
current = running
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('cli.js calling start() in build.compiled.js', err)
|
||||
})
|
||||
.finally(() => _start.stop())
|
||||
} catch (err) {
|
||||
_start.stop()
|
||||
}
|
||||
}
|
||||
|
||||
timer('cli.js').wrap(() => {
|
||||
timer('esbuild build.compiled.js').wrap(() => {
|
||||
const {build} = require('esbuild')
|
||||
|
||||
// compile build files directly which is about 10x faster than esbuild-register
|
||||
build({
|
||||
entryPoints: [__dirname + '/build.ts'],
|
||||
outfile: __dirname + '/build.compiled.js',
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
external: ['esbuild', 'react', 'react-dom/server'],
|
||||
watch: dev && {
|
||||
onRebuild(err, res) {
|
||||
if (!err) {
|
||||
onUpdatedBuildScript(true)
|
||||
}
|
||||
},
|
||||
},
|
||||
}).then(() => {
|
||||
onUpdatedBuildScript(false)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -5,23 +5,22 @@ export function createEsbuildLiveReloadTools(): {
|
|||
handleRequest(req: IncomingMessage, res: ServerResponse): boolean
|
||||
hasOpenConnections(): boolean
|
||||
esbuildBanner: esbuild.BuildOptions['banner']
|
||||
esbuildWatch: esbuild.WatchMode
|
||||
} {
|
||||
const openResponses = new Set<ServerResponse>()
|
||||
return {
|
||||
handleRequest(req, res) {
|
||||
// If special /esbuild url requested, subscribe clients to changes
|
||||
if (req.url === '/esbuild') {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
})
|
||||
res.write('data: open\n\n')
|
||||
openResponses.add(res)
|
||||
res.on('close', () => openResponses.delete(res))
|
||||
return true // handled
|
||||
}
|
||||
// if (req.url === '/esbuild') {
|
||||
// res.writeHead(200, {
|
||||
// 'Content-Type': 'text/event-stream',
|
||||
// 'Cache-Control': 'no-cache',
|
||||
// Connection: 'keep-alive',
|
||||
// })
|
||||
// res.write('data: open\n\n')
|
||||
// openResponses.add(res)
|
||||
// res.on('close', () => openResponses.delete(res))
|
||||
// return true // handled
|
||||
// }
|
||||
return false
|
||||
},
|
||||
hasOpenConnections() {
|
||||
|
@ -32,6 +31,7 @@ export function createEsbuildLiveReloadTools(): {
|
|||
// This is being used so we can at least get a few type completions, but please understand that
|
||||
// you cannot reference any non-global browser values from within the function.
|
||||
js: `;(${function liveReloadClientSetup() {
|
||||
console.log('%cLive reload enabled', 'color: gray')
|
||||
// from packages/playground/devEnv/createEsbuildLiveReloadTools.ts
|
||||
function connect() {
|
||||
if (window.parent !== window) {
|
||||
|
@ -39,24 +39,13 @@ export function createEsbuildLiveReloadTools(): {
|
|||
'%cLive reload disabled for iframed content',
|
||||
'color: gray',
|
||||
)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const es = new EventSource('/esbuild')
|
||||
es.onmessage = (evt) => {
|
||||
switch (evt.data) {
|
||||
case 'reload':
|
||||
location.reload()
|
||||
break
|
||||
case 'open':
|
||||
console.log('%cLive reload ready', 'color: gray')
|
||||
break
|
||||
}
|
||||
}
|
||||
es.onerror = () => {
|
||||
es.close()
|
||||
attemptConnect()
|
||||
}
|
||||
es.addEventListener('change', () => {
|
||||
console.log('%cLive reload triggered', 'color: gray')
|
||||
window.location.reload()
|
||||
})
|
||||
} catch (err) {
|
||||
attemptConnect()
|
||||
}
|
||||
|
@ -67,19 +56,5 @@ export function createEsbuildLiveReloadTools(): {
|
|||
attemptConnect()
|
||||
}.toString()})();`,
|
||||
},
|
||||
esbuildWatch: {
|
||||
onRebuild(error, res) {
|
||||
if (!error) {
|
||||
if (openResponses.size > 0) {
|
||||
console.error(`Reloading for ${openResponses.size} clients...`)
|
||||
// Notify clients on rebuild
|
||||
openResponses.forEach((res) => res.write('data: reload\n\n'))
|
||||
openResponses.clear()
|
||||
}
|
||||
} else {
|
||||
console.error('Rebuild had errors...')
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import type {Server, Socket} from 'net'
|
||||
|
||||
export function createServerForceClose(server: Server) {
|
||||
const openConnections = new Set<Socket>()
|
||||
server.on('connection', (conn) => {
|
||||
openConnections.add(conn)
|
||||
conn.on('close', () => openConnections.delete(conn))
|
||||
})
|
||||
|
||||
return function serverForceClose(): Promise<void> {
|
||||
for (const openConnection of openConnections) {
|
||||
openConnection.destroy()
|
||||
}
|
||||
|
||||
return new Promise((res) => {
|
||||
server.close(() => {
|
||||
res()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
5
packages/playground/devEnv/timer.d.ts
vendored
5
packages/playground/devEnv/timer.d.ts
vendored
|
@ -1,5 +0,0 @@
|
|||
/** Create timer */
|
||||
export function timer(name: string): {
|
||||
wrap<T>(fn: () => T): T
|
||||
stop(): void
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
/** @param {string} name */
|
||||
function timer(name) {
|
||||
const startMs = Date.now()
|
||||
console.group(`▶️ ${name}`)
|
||||
let stopped = false
|
||||
return {
|
||||
/**
|
||||
* @type {<T> (fn: () => T): T}
|
||||
*/
|
||||
wrap(fn) {
|
||||
const result = fn()
|
||||
this.stop()
|
||||
return result
|
||||
},
|
||||
stop() {
|
||||
if (stopped) return
|
||||
stopped = true
|
||||
console.groupEnd()
|
||||
console.log(
|
||||
`✓ ${name} in ${((Date.now() - startMs) * 0.001).toFixed(3)}s`,
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
exports.timer = timer
|
|
@ -8,9 +8,9 @@
|
|||
"dist/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"serve": "node devEnv/cli.js --serve --dev",
|
||||
"serve:ci": "node devEnv/cli.js --serve",
|
||||
"build": "node devEnv/cli.js",
|
||||
"serve": "node -r esbuild-register devEnv/build.ts --serve --dev",
|
||||
"serve:ci": "node -r esbuild-register devEnv/build.ts --serve",
|
||||
"build": "node -r esbuild-register devEnv/build.ts",
|
||||
"build:static": "echo 'building for vercel' && yarn run build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "playwright test --config=devEnv/playwright.config.ts",
|
||||
|
@ -29,7 +29,8 @@
|
|||
"@types/lodash-es": "^4.17.4",
|
||||
"@types/node": "^15.6.2",
|
||||
"@types/react": "^17.0.9",
|
||||
"esbuild": "^0.13.15",
|
||||
"esbuild": "^0.17.6",
|
||||
"esbuild-register": "^3.4.2",
|
||||
"three": "^0.130.1",
|
||||
"typescript": "^4.4.2"
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue