diff --git a/apps/docs/components/Playground.js b/apps/docs/components/Playground.js new file mode 100644 index 00000000..169e8548 --- /dev/null +++ b/apps/docs/components/Playground.js @@ -0,0 +1,269 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import BrowserOnly from '@docusaurus/BrowserOnly'; +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 { files } from './playground-utils/files'; + +library.add(faBars, faRotateRight); + +async function wcSpawn(instance, ...args) { + console.log('Running:', args.join(' ')); + const process = await instance.spawn(...args); + process.output.pipeTo( + new WritableStream({ + write(data) { + console.log(data); + }, + }), + ); + const exitCode = await process.exit; + if (exitCode !== 0) { + console.log('Command Failed:', args.join(' '), 'with exit code', exitCode); + throw new Error('Command Failed', args.join(' ')); + } + + console.log('Command Successful:', args.join(' ')); + return process; +} + +async function makeWebcontainer() { + console.log('Booting WebContainer...'); + const instance = await WebContainer.boot(); + console.log('Boot successful!'); + + console.log('Mounting files...'); + await instance.mount(files); + console.log('Mounted files!'); + + console.log('Installing dependencies...'); + await wcSpawn(instance, 'npm', ['install']); + console.log('Installed dependencies!'); + + return instance; +} + +export default function Playground() { + const instance = useRef(null); + const [url, setUrl] = useState(null); + const debounceTimeout = useRef(null); + const [code, setCode] = useState( + files.src.directory['App.jsx'].file.contents, + ); + const [error, setError] = useState(null); + const loadingTimeout = useRef(null); + const urlRef = useRef(null); + + const build = async () => { + const containerInstance = instance.current; + 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 run dev`...'); + const process = await containerInstance.spawn('npm', ['run', 'dev']); + console.log('Spawned `npm run dev`...'); + process.output.pipeTo( + new WritableStream({ + write(data) { + console.log(data); + }, + }), + ); + console.log('Waiting for server-ready event...'); + containerInstance.on('server-ready', (port, url) => { + console.log('server-ready', port, url); + setUrl(url); + urlRef.current = url; + }); + }; + + const updateFiles = async () => { + const containerInstance = instance.current; + const filePath = './src/App.jsx'; + const updatedCode = code; + await containerInstance.fs.writeFile(filePath, updatedCode); + }; + + const handleCodeChange = (newCode) => { + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + debounceTimeout.current = null; + } + + debounceTimeout.current = setTimeout(async () => { + setCode(newCode); + if (url) { + try { + await updateFiles(); + console.log('Successfully applied changes.'); + } catch (err) { + console.error(err); + } + } + }, 2000); + }; + + const reloadWebContainer = async () => { + if (!url) return; + const iframe = document.querySelector('iframe'); + if (!iframe) return; + try { + if (error) { + setError(null); + } + console.log('Reloading container preview...'); + 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(() => { + require('codemirror/mode/javascript/javascript'); + makeWebcontainer().then((i) => { + instance.current = i; + build().then(() => { + loadingTimeout.current = setTimeout(() => { + console.log('running loading timeout'); + console.log('instance: ', instance.current); + console.log('url ref: ', urlRef.current); + if (!urlRef.current) { + console.log('error due to timeout...'); + setError( + 'WebContainer failed to load. Please try reloading or use a different browser.', + ); + } + }, 10000); + }); + }); + + () => { + instance.current.unmount(); + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + if (loadingTimeout.current) { + clearTimeout(loadingTimeout.current); + } + }; + }, []); + + return ( +
+
+ + +
+
+ + {() => ( + <> + handleCodeChange(newCode)} + /> + {error ? ( +
{error}
+ ) : url ? ( +