From 162174568bb870c83e63561dd1a0dad1c533f31e Mon Sep 17 00:00:00 2001 From: Andrew Prifer <2991360+AndrewPrifer@users.noreply.github.com> Date: Mon, 13 Jun 2022 14:47:07 +0200 Subject: [PATCH] Replace Vite with ESBuild for the playground (#213) --- packages/playground/devEnv/Home.tsx | 29 +++ packages/playground/devEnv/build.tsx | 197 ++++++++++++++++++ .../playground/{src => devEnv}/index.html | 2 +- packages/playground/devEnv/vite.config.ts | 46 ---- packages/playground/package.json | 9 +- packages/playground/src/index.tsx | 92 -------- packages/playground/vercel.json | 3 - 7 files changed, 231 insertions(+), 147 deletions(-) create mode 100644 packages/playground/devEnv/Home.tsx create mode 100644 packages/playground/devEnv/build.tsx rename packages/playground/{src => devEnv}/index.html (90%) delete mode 100644 packages/playground/devEnv/vite.config.ts delete mode 100644 packages/playground/src/index.tsx delete mode 100644 packages/playground/vercel.json diff --git a/packages/playground/devEnv/Home.tsx b/packages/playground/devEnv/Home.tsx new file mode 100644 index 0000000..e7b7c49 --- /dev/null +++ b/packages/playground/devEnv/Home.tsx @@ -0,0 +1,29 @@ +import React from 'react' + +export const Home = ({groups}: {groups: {[groupName: string]: string[]}}) => ( + +) + +const Group = (props: {groupName: string; modules: string[]}) => { + const {groupName, modules} = props + return ( + + ) +} diff --git a/packages/playground/devEnv/build.tsx b/packages/playground/devEnv/build.tsx new file mode 100644 index 0000000..c88bcbc --- /dev/null +++ b/packages/playground/devEnv/build.tsx @@ -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( + 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(/[\s\S]*<\/body>/, `${homeHtml}`), + ), + // 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) + } + }) + }) diff --git a/packages/playground/src/index.html b/packages/playground/devEnv/index.html similarity index 90% rename from packages/playground/src/index.html rename to packages/playground/devEnv/index.html index aa0685a..26c1d45 100644 --- a/packages/playground/src/index.html +++ b/packages/playground/devEnv/index.html @@ -24,6 +24,6 @@
- + diff --git a/packages/playground/devEnv/vite.config.ts b/packages/playground/devEnv/vite.config.ts deleted file mode 100644 index cc51854..0000000 --- a/packages/playground/devEnv/vite.config.ts +++ /dev/null @@ -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', - }, -}) diff --git a/packages/playground/package.json b/packages/playground/package.json index 20fb2c4..80dcc67 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -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", diff --git a/packages/playground/src/index.tsx b/packages/playground/src/index.tsx deleted file mode 100644 index 4655163..0000000 --- a/packages/playground/src/index.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/** - * TODO explain this file - * */ -/// - -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 = () => ( - -) - -const Group = (props: {groupName: string; modules: Record}) => { - const {groupName, modules} = props - return ( -
    - {Object.entries(modules).map(([moduleName, callback]) => ( -
  • - {moduleName} - {/* */} -
  • - ))} -
- ) -} - -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')) -} diff --git a/packages/playground/vercel.json b/packages/playground/vercel.json deleted file mode 100644 index 3955a13..0000000 --- a/packages/playground/vercel.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "routes": [{"src": "/[^.]+", "dest": "/", "status": 200}] -}