Skip to content

Commit

Permalink
feat: adjust build setup to use vite
Browse files Browse the repository at this point in the history
feat: allow file editing and updates the preview
update reload button

fix: cleanup and improve perf
  • Loading branch information
6ri4n authored and nmn committed Dec 16, 2024
1 parent 7512b03 commit 8c15b00
Show file tree
Hide file tree
Showing 7 changed files with 1,463 additions and 229 deletions.
25 changes: 13 additions & 12 deletions apps/docs/components/LogoDownloadModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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)',
Expand All @@ -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',
},
});
Expand Down
245 changes: 204 additions & 41 deletions apps/docs/components/Playground.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -51,79 +70,219 @@ 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) {
console.log(data);
},
}),
);

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 (
<div {...stylex.props(styles.container)}>
<BrowserOnly>
{() => (
<>
<CodeMirror
{...stylex.props(styles.textarea)}
options={{
mode: 'javascript',
theme: 'material-darker',
lineNumbers: true,
}}
value={files.src.directory['app.jsx'].file.contents}
/>
{url ? (
<iframe {...stylex.props(styles.textarea)} src={url} />
) : (
<div {...stylex.props(styles.textarea)}>Loading...</div>
)}
</>
)}
</BrowserOnly>
<div {...stylex.props(styles.root)}>
<header {...stylex.props(styles.header)}>
{/* <button>
<FontAwesomeIcon icon="fa-solid fa-bars" />
</button> */}
<button
{...stylex.props(styles.reloadButton)}
onClick={reloadWebContainer}
>
<FontAwesomeIcon
aria-label="reload"
icon="fa-solid fa-rotate-right"
/>
</button>
</header>
<div {...stylex.props(styles.container)}>
<BrowserOnly>
{() => (
<>
<CodeMirror
{...stylex.props(styles.textarea)}
onChange={(editor, data, newCode) => handleCodeChange(newCode)}
options={{
mode: 'javascript',
theme: 'material-darker',
lineNumbers: true,
}}
value={code}
/>
{error ? (
<div {...stylex.props(styles.textarea, styles.centered)}>
{error}
</div>
) : url ? (
<iframe {...stylex.props(styles.textarea)} src={url} />
) : (
<div {...stylex.props(styles.textarea, styles.centered)}>
Loading...
</div>
)}
</>
)}
</BrowserOnly>
</div>
</div>
);
}

// Style definitions for the Playground component
const styles = stylex.create({
root: {
minHeight: '100vh',
backgroundColor: 'var(--bg1)',
},
header: {
height: 40,
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
paddingInline: 4,
backgroundColor: 'var(--playground-container-bg)',
borderBottomWidth: 1,
borderBottomStyle: 'solid',
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
boxShadow: '0 1px 4px rgba(0, 0, 0, 0.1)',
zIndex: 20,
},
reloadButton: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: {
default: 'transparent',
':hover': 'var(--ifm-color-primary-light)',
},
color: 'var(--fg1)',
width: 32,
height: 32,
borderRadius: 4,
borderStyle: 'none',
cursor: 'pointer',
zIndex: 20,
padding: 4,
transitionProperty: 'background-color, transform',
transitionDuration: '200ms, 150ms',
transform: {
default: null,
':hover': 'scale(1.05)',
},
},
container: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: stylex.firstThatWorks('calc(100dvh - 60px)', 'calc(100vh - 60px)'),
height: stylex.firstThatWorks(
'calc(100dvh - 100px)',
'calc(100vh - 100px)',
),
borderBottomWidth: 2,
borderBottomStyle: 'solid',
borderBottomColor: 'var(--cyan)',
Expand All @@ -138,4 +297,8 @@ const styles = stylex.create({
borderWidth: 0,
borderStyle: 'none',
},
centered: {
alignItems: 'center',
justifyContent: 'center',
},
});
Loading

0 comments on commit 8c15b00

Please sign in to comment.