diff --git a/.github/actions/yarn-nm-install/action.yml b/.github/actions/yarn-nm-install/action.yml index 419760c..726c67b 100644 --- a/.github/actions/yarn-nm-install/action.yml +++ b/.github/actions/yarn-nm-install/action.yml @@ -52,3 +52,7 @@ runs: if: steps.cache-playwright-browsers.outputs.cache-hit != 'true' shell: bash run: yarn workspace playground run playwright install --with-deps + + - name: Update browserlist + shell: bash + run: npx browserslist@latest --update-db diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fa0155..1a57190 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - uses: actions/checkout@v3 @@ -28,7 +28,7 @@ jobs: strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - uses: actions/checkout@v3 @@ -45,7 +45,7 @@ jobs: strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - uses: actions/checkout@v3 @@ -61,7 +61,7 @@ jobs: strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - uses: actions/checkout@v3 @@ -78,7 +78,7 @@ jobs: strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - uses: actions/checkout@v3 @@ -100,7 +100,7 @@ jobs: strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/release-insiders.yml b/.github/workflows/release-insiders.yml index 1ea6c41..a45db21 100644 --- a/.github/workflows/release-insiders.yml +++ b/.github/workflows/release-insiders.yml @@ -102,7 +102,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v3 with: - node-version: 16.x + node-version: 18.x - uses: ./.github/actions/yarn-nm-install - name: Build the Theatre.js packages diff --git a/compatibility-tests/package.json b/compatibility-tests/package.json index fac8883..79ce622 100644 --- a/compatibility-tests/package.json +++ b/compatibility-tests/package.json @@ -9,7 +9,7 @@ "dependencies": { "@cspotcode/zx": "^6.1.2", "node-cleanup": "^2.1.2", - "playwright": "^1.28.1", + "playwright": "^1.29.1", "prettier": "^2.6.2", "verdaccio": "^5.10.2", "verdaccio-auth-memory": "^10.2.0", diff --git a/jest.config.js b/jest.config.js index 3e672ce..269bfaf 100644 --- a/jest.config.js +++ b/jest.config.js @@ -20,15 +20,21 @@ module.exports = { automock: false, transform: { '^.+\\.tsx?$': [ - 'esbuild-jest', + 'jest-esbuild', { sourcemap: true, + supported: { + 'dynamic-import': false, + }, }, ], '^.+\\.js$': [ - 'esbuild-jest', + 'jest-esbuild', { sourcemap: true, + supported: { + 'dynamic-import': false, + }, }, ], }, diff --git a/package.json b/package.json index 80ebaf0..a583339 100644 --- a/package.json +++ b/package.json @@ -44,16 +44,17 @@ "@microsoft/api-extractor": "^7.28.6", "@typescript-eslint/eslint-plugin": "^5.30.7", "@typescript-eslint/parser": "^5.30.7", - "esbuild": "^0.16.7", - "esbuild-jest": "^0.5.0", + "esbuild": "^0.18.13", "eslint": "^8.20.0", "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-react": "^7.30.1", "eslint-plugin-tsdoc": "^0.2.16", "eslint-plugin-unused-imports": "^2.0.0", + "fast-glob": "^3.3.0", "husky": "^6.0.0", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", + "jest-esbuild": "^0.3.0", "jsonc-parser": "^3.1.0", "lint-staged": "^13.0.3", "node-gyp": "^9.1.0", diff --git a/packages/playground/.gitignore b/packages/playground/.gitignore index c82b321..6808d6c 100644 --- a/packages/playground/.gitignore +++ b/packages/playground/.gitignore @@ -1,4 +1,5 @@ /dist /test-results/ /playwright-report/ -/build \ No newline at end of file +/build +/dist \ No newline at end of file diff --git a/packages/playground/devEnv/build.ts b/packages/playground/devEnv/build.ts deleted file mode 100644 index 1bb78e7..0000000 --- a/packages/playground/devEnv/build.ts +++ /dev/null @@ -1,339 +0,0 @@ -import type {BuildOptions} from 'esbuild' -import esbuild from 'esbuild' -import {readdir, readFile, stat, writeFile} from 'fs/promises' -import {mapValues} from 'lodash-es' -import path from 'path' -import React from 'react' -import {renderToStaticMarkup} from 'react-dom/server' -import {ServerStyleSheet} from 'styled-components' -import {definedGlobals} from '../../../theatre/devEnv/definedGlobals' -import {createEsbuildLiveReloadTools} from './createEsbuildLiveReloadTools' -import {createProxyServer} from './createProxyServer' -import {PlaygroundPage} from './home/PlaygroundPage' -import {openForOS} from './openForOS' -import {tryMultiplePorts} from './tryMultiplePorts' - -const playgroundDir = (folder: string) => path.join(__dirname, '..', folder) -const buildDir = playgroundDir('build') -const srcDir = playgroundDir('src') -const sharedDir = playgroundDir('src/shared') -const personalDir = playgroundDir('src/personal') -const testDir = playgroundDir('src/tests') - -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 */ - isVisualRegressionTesting: boolean - serve?: { - findAvailablePort: boolean - openBrowser: boolean - /** defaults to 8080 */ - defaultPort?: number - } -}): Promise { - const defaultPort = options.serve?.defaultPort ?? 8080 - - const liveReload = - options.serve && options.dev ? createEsbuildLiveReloadTools() : undefined - - type PlaygroundExample = { - useHtml?: string - entryFilePath: string - outDir: string - } - - type Groups = { - [group: string]: { - [module: string]: PlaygroundExample - } - } - - // Collect all entry directories per module per group - const groups: Groups = await Promise.all( - [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, - ), - } - - 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), - ), - ) - - // Collect all entry files - const entryPoints = Object.values(groups) - .flatMap((group) => Object.values(group)) - .map((module) => module.entryFilePath) - - // Collect all output directories - const outModules: PlaygroundExample[] = Object.values(groups).flatMap( - (group) => Object.values(group), - ) - - // Render home page contents - const homeHtml = (() => { - const sheet = new ServerStyleSheet() - try { - const html = renderToStaticMarkup( - sheet.collectStyles( - React.createElement(PlaygroundPage, { - groups: mapValues(groups, (group) => Object.keys(group)), - }), - ), - ) - const styleTags = sheet.getStyleTags() // or sheet.getStyleElement(); - sheet.seal() - return { - head: styleTags, - html, - } - } catch (error) { - // handle error - console.error(error) - sheet.seal() - process.exit(1) - } - })() - - const esbuildConfig: BuildOptions = { - entryPoints, - bundle: true, - sourcemap: true, - outdir: buildDir, - target: ['firefox88'], - loader: { - '.png': 'file', - '.glb': 'file', - '.gltf': 'file', - '.mp3': 'file', - '.ogg': 'file', - '.svg': 'dataurl', - }, - define: { - ...definedGlobals, - 'window.__IS_VISUAL_REGRESSION_TESTING': JSON.stringify( - options.isVisualRegressionTesting, - ), - '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) - // }, - // }, - plugins: [ - { - name: 'watch playground assets', - setup(build) { - build.onStart(() => {}) - build.onLoad( - { - filter: /index\.tsx?$/, - }, - async (loadFile) => { - const indexHtmlPath = loadFile.path.replace( - /index\.tsx?$/, - 'index.html', - ) - const relToSrc = path.relative(srcDir, indexHtmlPath) - const isInSrcFolder = !relToSrc.startsWith('..') - if (isInSrcFolder) { - const newHtml = await readFile(indexHtmlPath, 'utf-8').catch( - () => undefined, - ) - if (newHtml) { - await writeFile( - path.resolve(buildDir, relToSrc), - newHtml.replace( - /<\/body>/, - ``, - ), - ).catch( - wrapCatch( - `loading index.tsx creates corresponding index.html for ${relToSrc}`, - ), - ) - } - - return { - watchFiles: [indexHtmlPath], - } - } - }, - ) - }, - }, - ], - } - - const ctx = await esbuild.context(esbuildConfig) - - 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(//, `${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>/, - ``, - ), - '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) { - await ctx.dispose() - return - } - - const {serve} = options - - // 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 ctx.serve({servedir: buildDir}) - - const proxyServer = createProxyServer(liveReload?.handleRequest, { - hostname: '0.0.0.0', - port: esbuildServe.port, - }) - - // const proxyForceExit = createServerForceClose(proxyServer) - const portTries = serve.findAvailablePort ? 10 : 1 - const portChosen = await tryMultiplePorts(defaultPort, portTries, proxyServer) - - const hostedAt = `http://localhost:${portChosen}` - - console.log('Playground running at', hostedAt) - - if (serve.openBrowser) { - setTimeout(() => { - if (!liveReload?.hasOpenConnections()) openForOS(hostedAt) - }, 1000) - } - - // return { - // async stop() { - // esbuildWatchStop?.() - // await proxyForceExit() - // }, - // } -} - -function wrapCatch(message: string) { - return (err: any) => { - 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) - }, -) diff --git a/packages/playground/devEnv/createEsbuildLiveReloadTools.ts b/packages/playground/devEnv/createEsbuildLiveReloadTools.ts deleted file mode 100644 index 7bf0ced..0000000 --- a/packages/playground/devEnv/createEsbuildLiveReloadTools.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type esbuild from 'esbuild' -import type {IncomingMessage, ServerResponse} from 'http' - -export function createEsbuildLiveReloadTools(): { - handleRequest(req: IncomingMessage, res: ServerResponse): boolean - hasOpenConnections(): boolean - esbuildBanner: esbuild.BuildOptions['banner'] -} { - const openResponses = new Set() - 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 - // } - return false - }, - hasOpenConnections() { - return openResponses.size > 0 - }, - esbuildBanner: { - // Below uses function toString to insert raw source code of the function into the JS source. - // 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) { - console.log( - '%cLive reload disabled for iframed content', - 'color: gray', - ) - } - try { - const es = new EventSource('/esbuild') - es.addEventListener('change', () => { - console.log('%cLive reload triggered', 'color: gray') - window.location.reload() - }) - } catch (err) { - attemptConnect() - } - } - function attemptConnect() { - setTimeout(() => connect(), 1000) - } - attemptConnect() - }.toString()})();`, - }, - } -} diff --git a/packages/playground/devEnv/createProxyServer.ts b/packages/playground/devEnv/createProxyServer.ts deleted file mode 100644 index 5fe07ef..0000000 --- a/packages/playground/devEnv/createProxyServer.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type {IncomingMessage, ServerResponse} from 'http' -import {createServer, request} from 'http' - -// See example from https://esbuild.github.io/api/#customizing-server-behavior -export function createProxyServer( - handleRequest: - | ((req: IncomingMessage, res: ServerResponse) => boolean) - | undefined, - target: {hostname: string; port: number}, -) { - return createServer((req, res) => { - const {url, method, headers} = req - if (handleRequest?.(req, res)) { - return - } - - // Otherwise forward requests to target (e.g. ESBuild server) - req.pipe( - request( - { - ...target, - path: url, - method, - headers, - }, - (prxRes) => { - res.writeHead(prxRes.statusCode!, prxRes.headers) - prxRes.pipe(res, {end: true}) - }, - ), - {end: true}, - ) - }) -} diff --git a/packages/playground/devEnv/home/PlaygroundPage.tsx b/packages/playground/devEnv/home/PlaygroundPage.tsx deleted file mode 100644 index 09726fb..0000000 --- a/packages/playground/devEnv/home/PlaygroundPage.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react' -import styled, {StyleSheetManager} from 'styled-components' -import {ItemSectionWithPreviews} from './ItemSectionWithPreviews' -import {PlaygroundHeader} from './PlaygroundHeader' - -const HomeContainer = styled.div` - position: fixed; - inset: 0; - background: #1b1c1e; - overflow: auto; -` -const ContentContainer = styled.div` - padding: 0 5rem; - - @media screen and (max-width: 920px) { - padding: 0 2rem; - } -` - -const version = require('../../../../theatre/studio/package.json').version - -const PageTitleH1 = styled.h1` - padding: 1rem 0; -` - -export const PlaygroundPage = ({ - groups, -}: { - groups: {[groupName: string]: string[]} -}) => ( - - - - - Playground - {Object.entries(groups).map(([groupName, modules]) => ( - - ))} - - - -) diff --git a/packages/playground/devEnv/openForOS.ts b/packages/playground/devEnv/openForOS.ts deleted file mode 100644 index b848027..0000000 --- a/packages/playground/devEnv/openForOS.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {spawn} from 'child_process' - -export function openForOS(hostedAt: string) { - const open = { - darwin: ['open', '-a', 'Google Chrome'], - 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})`, - ) - } -} diff --git a/packages/playground/devEnv/playwright.config.ts b/packages/playground/devEnv/playwright.config.ts index fe749d3..6de841e 100644 --- a/packages/playground/devEnv/playwright.config.ts +++ b/packages/playground/devEnv/playwright.config.ts @@ -68,7 +68,7 @@ const config: PlaywrightTestConfig = { TODO 👆 */ webServer: { - command: 'yarn run serve:ci', + command: 'yarn run serve:ci --port 8080', port: 8080, reuseExistingServer: !process.env.CI, }, diff --git a/packages/playground/devEnv/tryMultiplePorts.ts b/packages/playground/devEnv/tryMultiplePorts.ts deleted file mode 100644 index 7eaf54e..0000000 --- a/packages/playground/devEnv/tryMultiplePorts.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type {Server} from 'net' - -export async function tryMultiplePorts( - port: number, - tries: number, - server: Server, -): Promise { - 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 -} diff --git a/packages/playground/package.json b/packages/playground/package.json index 08776a5..c422fd9 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -8,9 +8,9 @@ "dist/**/*" ], "scripts": { - "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", + "serve": "vite", + "serve:ci": "vite build && vite preview", + "build": "vite build --force", "build:static": "echo 'building for vercel' && yarn run build", "typecheck": "tsc --noEmit", "test": "playwright test --config=devEnv/playwright.config.ts", @@ -22,6 +22,7 @@ "@playwright/test": "^1.29.1", "@react-three/drei": "^7.2.2", "@react-three/fiber": "^7.0.6", + "@rollup/plugin-virtual": "^3.0.1", "@theatre/core": "workspace:*", "@theatre/r3f": "workspace:*", "@theatre/studio": "workspace:*", @@ -29,9 +30,19 @@ "@types/lodash-es": "^4.17.4", "@types/node": "^15.6.2", "@types/react": "^17.0.9", + "@vitejs/plugin-react": "^4.0.0", + "@vitejs/plugin-react-swc": "^3.3.2", "esbuild": "^0.17.6", "esbuild-register": "^3.4.2", + "parcel": "^2.9.3", "three": "^0.130.1", - "typescript": "^4.4.2" + "typescript": "^4.4.2", + "vite": "^4.3.9", + "vite-plugin-commonjs": "^0.8.0", + "vite-plugin-html-template": "^1.2.0", + "vite-plugin-mpa": "^1.2.0" + }, + "dependencies": { + "@originjs/vite-plugin-commonjs": "^1.0.3" } } diff --git a/packages/playground/devEnv/home/ItemSectionWithPreviews.tsx b/packages/playground/src/home/ItemSectionWithPreviews.tsx similarity index 98% rename from packages/playground/devEnv/home/ItemSectionWithPreviews.tsx rename to packages/playground/src/home/ItemSectionWithPreviews.tsx index 50f1347..2f19cd2 100644 --- a/packages/playground/devEnv/home/ItemSectionWithPreviews.tsx +++ b/packages/playground/src/home/ItemSectionWithPreviews.tsx @@ -11,7 +11,7 @@ export const ItemSectionWithPreviews = (props: { {groupName} {modules.map((moduleName) => { - const href = `/${groupName}/${moduleName}` + const href = `/${groupName}/${moduleName}/` return ( diff --git a/packages/playground/devEnv/home/PlaygroundHeader.tsx b/packages/playground/src/home/PlaygroundHeader.tsx similarity index 100% rename from packages/playground/devEnv/home/PlaygroundHeader.tsx rename to packages/playground/src/home/PlaygroundHeader.tsx diff --git a/packages/playground/src/home/PlaygroundPage.tsx b/packages/playground/src/home/PlaygroundPage.tsx new file mode 100644 index 0000000..42959a7 --- /dev/null +++ b/packages/playground/src/home/PlaygroundPage.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import styled, {StyleSheetManager} from 'styled-components' +import {ItemSectionWithPreviews} from './ItemSectionWithPreviews' +import {PlaygroundHeader} from './PlaygroundHeader' +// @ts-ignore +import {version} from '../../../../theatre/studio/package.json' + +const HomeContainer = styled.div` + position: fixed; + inset: 0; + background: #1b1c1e; + overflow: auto; +` +const ContentContainer = styled.div` + padding: 0 5rem; + + @media screen and (max-width: 920px) { + padding: 0 2rem; + } +` + +// const {version} = require('') + +const PageTitleH1 = styled.h1` + padding: 1rem 0; +` + +export const PlaygroundPage = ({ + groups, +}: { + groups: {[groupName: string]: string[]} +}) => { + return ( + + + + + Playground + {Object.entries(groups).map(([groupName, modules]) => ( + + ))} + + + + ) +} diff --git a/packages/playground/devEnv/index.html b/packages/playground/src/index.html similarity index 87% rename from packages/playground/devEnv/index.html rename to packages/playground/src/index.html index 26f56ac..11be738 100644 --- a/packages/playground/devEnv/index.html +++ b/packages/playground/src/index.html @@ -2,8 +2,10 @@ - + Playground – Theatre.js + +