Replace Vite with ESBuild for the playground (#213)

This commit is contained in:
Andrew Prifer 2022-06-13 14:47:07 +02:00 committed by GitHub
parent a6951effd8
commit 162174568b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 231 additions and 147 deletions

View file

@ -0,0 +1,29 @@
import React from 'react'
export const Home = ({groups}: {groups: {[groupName: string]: string[]}}) => (
<ul>
{Object.entries(groups).map(([groupName, modules]) => (
<li key={`li-${groupName}`}>
<span>{groupName}</span>
<Group
key={`group-${groupName}`}
groupName={groupName}
modules={modules}
/>
</li>
))}
</ul>
)
const Group = (props: {groupName: string; modules: string[]}) => {
const {groupName, modules} = props
return (
<ul>
{modules.map((moduleName) => (
<li key={`li-${moduleName}`}>
<a href={`/${groupName}/${moduleName}`}>{moduleName}</a>
</li>
))}
</ul>
)
}

View file

@ -0,0 +1,197 @@
import {readdirSync} from 'fs'
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 {definedGlobals} from '../../../theatre/devEnv/buildUtils'
import {mapValues} from 'lodash-es'
import {createServer, request} from 'http'
import {spawn} from 'child_process'
import React from 'react'
import {renderToStaticMarkup} from 'react-dom/server'
import {Home} from './Home'
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 dev = /^--dev|-d$/.test(process.argv[process.argv.length - 1])
const port = 8080
const clients: ServerResponse[] = []
type Groups = {
[group: string]: {
[module: string]: {
entryDir: string
outDir: string
}
}
}
// Collect all entry directories per module per group
const groups: Groups = Object.fromEntries(
[sharedDir, personalDir, testDir]
.map((groupDir) => {
try {
return [
path.basename(groupDir),
Object.fromEntries(
readdirSync(groupDir).map((moduleDir) => [
path.basename(moduleDir),
{
entryDir: path.join(groupDir, moduleDir),
outDir: path.join(buildDir, path.basename(groupDir), moduleDir),
},
]),
),
]
} catch (e) {
// If the group dir doesn't exist, we just set its entry to undefined
return [path.basename(groupDir), undefined]
}
})
// and then filter it out.
.filter((entry) => entry[1] !== undefined),
)
// Collect all entry files
const entryPoints = Object.values(groups)
.flatMap((group) => Object.values(group))
.map((module) => path.join(module.entryDir, 'index.tsx'))
// Collect all output directories
const outDirs = Object.values(groups).flatMap((group) =>
Object.values(group).map((module) => module.outDir),
)
// Render home page contents
const homeHtml = renderToStaticMarkup(
<Home groups={mapValues(groups, (group) => Object.keys(group))} />,
)
const config: BuildOptions = {
entryPoints,
bundle: true,
sourcemap: true,
outdir: path.join(playgroundDir, 'build'),
target: ['firefox88'],
loader: {
'.png': 'file',
'.glb': 'file',
'.gltf': 'file',
'.svg': 'dataurl',
},
define: {
...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...')
},
},
}
esbuild
.build(config)
.then(async () => {
// Read index.html template
const index = await readFile(path.join(__dirname, 'index.html'), 'utf8')
await Promise.all([
// Write home page
writeFile(
path.join(buildDir, 'index.html'),
index.replace(/<body>[\s\S]*<\/body>/, `<body>${homeHtml}</body>`),
),
// Write module pages
...outDirs.map((outDir) =>
writeFile(
path.join(outDir, 'index.html'),
// Substitute %ENTRYPOINT% placeholder with the output file path
index.replace(
'%ENTRYPOINT%',
path.join('/', path.relative(buildDir, outDir), 'index.js'),
),
),
),
])
})
.catch((err) => {
console.log(err)
return process.exit(1)
})
.then(() => {
// Only start dev server in dev, otherwise just run build and that's it
if (!dev) {
return
}
// 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)
})
// 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)
}
})
})

View file

@ -24,6 +24,6 @@
<body>
<div id="root"></div>
<script type="module" src="./index.tsx"></script>
<script src="%ENTRYPOINT%"></script>
</body>
</html>

View file

@ -1,46 +0,0 @@
import {defineConfig} from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import {getAliasesFromTsConfigForRollup} from '../../../devEnv/getAliasesFromTsConfig'
import {definedGlobals} from '../../../theatre/devEnv/buildUtils'
/*
We're using vite instead of the older pure-esbuild setup. The tradeoff is
that page reloads are much slower (>1s diff), while hot reload of react components
are instantaneous and of course, they preserve state.
@todo Author feels that the slow reloads are quite annoying and disruptive to flow,
so if you find a way to make them faster, please do.
*/
const playgroundDir = path.join(__dirname, '..')
const port = 8080
// https://vitejs.dev/config/
export default defineConfig({
root: path.join(playgroundDir, './src'),
build: {
outDir: '../build',
minify: false,
emptyOutDir: true,
},
assetsInclude: ['**/*.gltf', '**/*.glb'],
server: {
port,
},
plugins: [react()],
resolve: {
/*
This will alias paths like `@theatre/core` to `path/to/theatre/core/src/index.ts` and so on,
so vite won't treat the monorepo's packages as externals and won't pre-bundle them.
*/
alias: [...getAliasesFromTsConfigForRollup()],
},
define: {
...definedGlobals,
'window.__IS_VISUAL_REGRESSION_TESTING': 'true',
},
})

View file

@ -8,13 +8,12 @@
"dist/**/*"
],
"scripts": {
"serve": "vite --config ./devEnv/vite.config.ts",
"build:static": "vite --config ./devEnv/vite.config.ts build",
"build:preview": "vite --config ./devEnv/vite.config.ts preview",
"serve": "node -r esbuild-register devEnv/build.tsx -d",
"build": "node -r esbuild-register devEnv/build.tsx",
"build:static": "yarn build",
"typecheck": "yarn run build",
"test": "playwright test --config=devEnv/playwright.config.ts",
"test:ci": "percy exec -- playwright test --reporter=dot --config=devEnv/playwright.config.ts --project=chromium",
"build": "tsc --build ./tsconfig.json"
"test:ci": "percy exec -- playwright test --reporter=dot --config=devEnv/playwright.config.ts --project=chromium"
},
"devDependencies": {
"@percy/cli": "^1.3.0",

View file

@ -1,92 +0,0 @@
/**
* TODO explain this file
* */
/// <reference types="vite/client" />
import type {$FixMe} from '@theatre/shared/utils/types'
import {mapKeys} from 'lodash-es'
import React from 'react'
import ReactDOM from 'react-dom'
const groups = {
shared: mapKeys(import.meta.glob('./shared/*/index.tsx'), (_, path) =>
pathToModuleName(path),
),
personal: mapKeys(import.meta.glob('./personal/*/index.tsx'), (_, path) =>
pathToModuleName(path),
),
tests: mapKeys(import.meta.glob('./tests/*/index.tsx'), (_, path) =>
pathToModuleName(path),
),
}
function pathToModuleName(path: string): string {
const matches = path.match(
/^\.\/(shared|personal|tests)\/([a-zA-Z0-9\-\s]+)\/index\.tsx$/,
)
if (!matches) {
throw new Error(
`module ${path} has invalid characters in its path. Valid names should match the regexp above this line.`,
)
}
return matches[2]
}
const Home = () => (
<ul>
{Object.entries(groups).map(([groupName, modules]) => (
<li key={`li-${groupName}`}>
<span>{groupName}</span>
<Group
key={`group-${groupName}`}
groupName={groupName}
modules={modules}
/>
</li>
))}
</ul>
)
const Group = (props: {groupName: string; modules: Record<string, $FixMe>}) => {
const {groupName, modules} = props
return (
<ul>
{Object.entries(modules).map(([moduleName, callback]) => (
<li key={`li-${moduleName}`}>
<a href={`/${groupName}/${moduleName}`}>{moduleName}</a>
{/* <Group key={`group-${group}`} modules={modules} /> */}
</li>
))}
</ul>
)
}
const currentPathname = document.location.pathname
if (currentPathname === '/') {
renderHome()
} else {
const parts = currentPathname.match(
/^\/(shared|personal|tests)\/([a-zA-Z0-9\-]+)$/,
)
if (parts) {
const [, groupName, moduleName] = parts
const group = groups[groupName as 'shared' | 'personal']
if (!group) {
throw new Error(`Unknown group ${groupName}`)
}
const module = group[moduleName]
if (!module) {
throw new Error(`Unknown module ${moduleName}`)
}
module()
} else {
throw new Error(`Unknown path ${currentPathname}`)
}
}
function renderHome() {
ReactDOM.render(React.createElement(Home), document.getElementById('root'))
}

View file

@ -1,3 +0,0 @@
{
"routes": [{"src": "/[^.]+", "dest": "/", "status": 200}]
}