From 8c15b00a1cc4ef97994ff12c8cea1fa54b03a583 Mon Sep 17 00:00:00 2001 From: Brian <6ri4n@users.noreply.github.com> Date: Mon, 4 Dec 2023 16:44:35 -0800 Subject: [PATCH] feat: adjust build setup to use vite feat: allow file editing and updates the preview update reload button fix: cleanup and improve perf --- apps/docs/components/LogoDownloadModal.js | 25 +- apps/docs/components/Playground.js | 245 +++++- apps/docs/components/hooks/useDebounced.js | 32 + .../docs/components/playground-utils/files.js | 605 +++++++++++--- apps/docs/package.json | 6 +- package-lock.json | 776 ++++++++++++++++-- package.json | 3 + 7 files changed, 1463 insertions(+), 229 deletions(-) create mode 100644 apps/docs/components/hooks/useDebounced.js diff --git a/apps/docs/components/LogoDownloadModal.js b/apps/docs/components/LogoDownloadModal.js index 1772dcb2..3941890d 100644 --- a/apps/docs/components/LogoDownloadModal.js +++ b/apps/docs/components/LogoDownloadModal.js @@ -16,8 +16,8 @@ const styles = stylex.create({ position: 'fixed', top: 60, left: 20, - padding: '16px', - borderRadius: '8px', + padding: 16, + borderRadius: 8, boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', maxWidth: '16.25rem', maxHeight: '80vh', @@ -39,12 +39,12 @@ const styles = stylex.create({ color: '#FFFFFF', }, section: { - marginBottom: '16px', + marginBottom: 16, }, sectionTitle: { - fontSize: '14px', + fontSize: 14, fontWeight: 'bold', - marginBottom: '8px', + marginBottom: 8, }, buttonGroup: { display: 'flex', @@ -54,7 +54,8 @@ const styles = stylex.create({ display: 'flex', alignItems: 'center', justifyContent: 'flex-start', - padding: '12px 16px', + paddingBlock: 12, + paddingInline: 16, cursor: 'pointer', backgroundColor: { default: 'var(--ifm-background-color)', @@ -64,17 +65,17 @@ const styles = stylex.create({ default: 'var(--ifm-font-color-base)', ':hover': '#FFFFFF', }, - borderRadius: '4px', - fontSize: '14px', + borderRadius: 4, + fontSize: 14, transition: 'background-color 0.2s, color 0.2s', borderStyle: 'none', width: '100%', - marginBottom: '8px', + marginBottom: 8, }, icon: { - marginRight: '8px', - width: '16px', - height: '16px', + marginRight: 8, + width: 16, + height: 16, fill: 'currentColor', }, }); diff --git a/apps/docs/components/Playground.js b/apps/docs/components/Playground.js index 00ec559e..b6ffd58f 100644 --- a/apps/docs/components/Playground.js +++ b/apps/docs/components/Playground.js @@ -7,14 +7,29 @@ * @format */ -import * as React from 'react'; -import {useEffect, useState, useRef} from 'react'; -import * as stylex from '@stylexjs/stylex'; +// Import necessary components and libraries import BrowserOnly from '@docusaurus/BrowserOnly'; -import {WebContainer} from '@webcontainer/api'; -import {files} from './playground-utils/files'; -import {UnControlled as CodeMirror} from 'react-codemirror2'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faBars, faRotateRight } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import * as stylex from '@stylexjs/stylex'; +import { WebContainer, reloadPreview } from '@webcontainer/api'; +import * as React from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { UnControlled as CodeMirror } from 'react-codemirror2'; +import 'codemirror/mode/javascript/javascript'; +import { files } from './playground-utils/files'; +import useDebounced from './hooks/useDebounced'; + +// Add FontAwesome icons to the library +library.add(faBars, faRotateRight); +/** + * Function to spawn a command in the WebContainer instance. + * @param {WebContainer} instance - The WebContainer instance. + * @param {...string} args - Command arguments to be executed. + * @returns {Promise} - Promise that resolves when the command execution is successful. + */ async function wcSpawn(instance, ...args) { console.log('Running:', args.join(' ')); const process = await instance.spawn(...args); @@ -35,6 +50,10 @@ async function wcSpawn(instance, ...args) { return process; } +/** + * Function to initialize and configure the WebContainer. + * @returns {Promise} - Promise that resolves with the configured WebContainer instance. + */ async function makeWebcontainer() { console.log('Booting WebContainer...'); const instance = await WebContainer.boot(); @@ -51,17 +70,35 @@ async function makeWebcontainer() { return instance; } +/** + * Main component for the Playground. + * @returns {JSX.Element} - The rendered JSX element. + */ export default function Playground() { const instance = useRef(null); const [url, setUrl] = useState(null); + const [code, _setCode] = useState( + files.src.directory['App.jsx'].file.contents, + ); + const [error, setError] = useState(null); + const urlRef = useRef(null); + /** + * Function to build the WebContainer and start the development server. + */ const build = async () => { const containerInstance = instance.current; - if (!containerInstance) return; + if (!containerInstance) { + console.log('error due to failed instance'); + setError( + 'WebContainer failed to load. Please try reloading or use a different browser.', + ); + return; + } - console.log('Trying to run `npm start`...'); - const process = await containerInstance.spawn('npm', ['start']); - console.log('Spawned `npm start`...'); + console.log('Trying to run `npm run dev`...'); + const process = await containerInstance.spawn('npm', ['run', 'dev']); + console.log('Spawned `npm run dev`...'); process.output.pipeTo( new WritableStream({ write(data) { @@ -69,61 +106,183 @@ export default function Playground() { }, }), ); - console.log('Waiting for server-ready event...'); containerInstance.on('server-ready', (port, url) => { console.log('server-ready', port, url); - // TODO: Figure out hot reloading - // TODO: Figure out how to start server *after* build - setTimeout(() => { - setUrl(url); - }, 5000); + setUrl(url); + urlRef.current = url; }); }; + /** + * Function to update files in the WebContainer. + */ + const updateFiles = async (updatedCode) => { + const containerInstance = instance.current; + const filePath = './src/App.jsx'; + await containerInstance.fs.writeFile(filePath, updatedCode); + }; + + const debouncedUpdateFiles = useDebounced(async (newCode) => { + await updateFiles(newCode); + }, 1000); + + /** + * Function to handle code changes in the CodeMirror editor. + * @param {string} newCode - The new code content from the editor. + */ + const handleCodeChange = (newCode) => { + // setCode(newCode); + debouncedUpdateFiles(newCode); + }; + + /** + * Function to reload the WebContainer preview. + */ + const reloadWebContainer = async () => { + if (!url) return; + const iframe = document.querySelector('iframe'); + if (!iframe) return; + try { + if (error) { + setError(null); + } + await reloadPreview(iframe); + } catch (err) { + console.error(`Error reloading preview: ${err.message}`); + setError( + 'WebContainer failed to load. Please try reloading or use a different browser.', + ); + } + }; + + // useEffect to initialize the WebContainer and build it useEffect(() => { - require('codemirror/mode/javascript/javascript'); + let loadingTimeout; makeWebcontainer().then((i) => { instance.current = i; - build(); + build().then(() => { + loadingTimeout = setTimeout(() => { + if (!urlRef.current) { + console.log('error due to timeout...'); + setError( + 'WebContainer failed to load. Please try reloading or use a different browser.', + ); + } + loadingTimeout = null; + }, 10000); + }); }); - () => { + + // Cleanup function to unmount the WebContainer and clear timeouts + return () => { instance.current.unmount(); + if (loadingTimeout != null) { + clearTimeout(loadingTimeout); + } }; }, []); + // Render the Playground component return ( -
- - {() => ( - <> - - {url ? ( -