Skip to content

Commit

Permalink
Configure local development for playground
Browse files Browse the repository at this point in the history
Add codemirror editor

chore: fix build

chore: add serve package to dependencies

feat: adjust build setup to use vite

feat: allow file editing and updates the preview

update apps/docs/components/Playground.js

Co-authored-by: Naman Goel <[email protected]>

update apps/docs/components/Playground.js

Co-authored-by: Naman Goel <[email protected]>

update apps/docs/components/Playground.js

Co-authored-by: Naman Goel <[email protected]>

update apps/docs/components/Playground.js

Co-authored-by: Naman Goel <[email protected]>

update apps/docs/components/Playground.js

feat: WIP preview with rollup setup
- note: rollup command not generating dist directory and associated files

feat: add preview functionality

chore: format App.jsx boilerplate

preview with vite setup

feat: WIP failed to load error message and reload preview button

chore: update package-lock.json

chore: refactor error handling

feat: WIP failed to load error message

feat: WIP failed to load error message

feat: failed to load error message and reload preview button

feat: failed to load error message

chore: add console logs

chore: check if iframe exists before reloading preview

chore: add error handling in reloadWebContainer function
  • Loading branch information
nmn committed Dec 16, 2024
1 parent 7512b03 commit bbc0460
Show file tree
Hide file tree
Showing 5 changed files with 1,384 additions and 262 deletions.
205 changes: 167 additions & 38 deletions apps/docs/components/Playground.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@
* @format
*/

import * as React from 'react';
import {useEffect, useState, useRef} from 'react';
import * as stylex from '@stylexjs/stylex';
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';

library.add(faBars, faRotateRight);

async function wcSpawn(instance, ...args) {
console.log('Running:', args.join(' '));
Expand Down Expand Up @@ -54,71 +60,194 @@ async function makeWebcontainer() {
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) 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;
});
};

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();
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 (
<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)}>
<FontAwesomeIcon
onClick={reloadWebContainer}
icon="fa-solid fa-rotate-right"
/>
</button>
</header>
<div {...stylex.props(styles.container)}>
<BrowserOnly>
{() => (
<>
<CodeMirror
{...stylex.props(styles.textarea)}
options={{
mode: 'javascript',
theme: 'material-darker',
lineNumbers: true,
}}
value={code}
onChange={(editor, data, newCode) => handleCodeChange(newCode)}
/>
{error ? (
<div {...stylex.props(styles.textarea)}>{error}</div>
) : url ? (
<iframe {...stylex.props(styles.textarea)} src={url} />
) : (
<div {...stylex.props(styles.textarea)}>Loading...</div>
)}
</>
)}
</BrowserOnly>
</div>
</div>
);
}

const styles = stylex.create({
root: {
minHeight: '100vh',
backgroundColor: 'var(--bg1)',
},
header: {
height: '40px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
paddingLeft: '20px',
paddingRight: '20px',
backgroundColor: 'var(--playground-container-bg)',
borderBottom: '1px solid 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: 'transparent',
color: 'var(--fg1)',
width: '32px',
height: '32px',
borderRadius: '4px',
border: 'none',
cursor: 'pointer',
zIndex: 20,
padding: '4px',
transition: 'background-color 200ms, transform 150ms',
':hover': {
backgroundColor: 'var(--ifm-color-primary-light)',
transform: 'scale(1.05)',
},
},
container: {
display: 'flex',
alignItems: 'center',
Expand Down
Loading

0 comments on commit bbc0460

Please sign in to comment.