diff --git a/apps/docs/components/Playground.js b/apps/docs/components/Playground.js new file mode 100644 index 00000000..4bf9424f --- /dev/null +++ b/apps/docs/components/Playground.js @@ -0,0 +1,179 @@ +/** + * 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 * as stylex from '@stylexjs/stylex'; +import { WebContainer } 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'; + +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 [isUpdating, setIsUpdating] = useState(null); + const [code, setCode] = useState( + files.src.directory['App.jsx'].file.contents, + ); + + const build = async () => { + const containerInstance = instance.current; + if (!containerInstance) 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); + setTimeout(() => { + setUrl(url); + }, 5000); + }); + }; + + 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) { + setIsUpdating(true); + try { + await updateFiles(); + console.log('Successfully applied changes.'); + } catch (err) { + console.error(err); + } finally { + setTimeout(() => { + setIsUpdating(false); + }, 5000); + } + } + }, 2000); + }; + + useEffect(() => { + require('codemirror/mode/javascript/javascript'); + makeWebcontainer().then((i) => { + instance.current = i; + build(); + }); + () => { + instance.current.unmount(); + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + }; + }, []); + + return ( +
+ + {() => ( + <> + handleCodeChange(newCode)} + /> + {url && !isUpdating ? ( +