Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow file editing and updates the preview #762

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
546076d
Docs: Add proper playground
nmn Nov 29, 2023
21e8cef
Improve compilation to not insert inject calls
nmn Nov 29, 2023
2ad472b
Add Vercel headers
nmn Nov 29, 2023
c052704
Configure local development for playground
nmn Dec 5, 2023
4310651
Add codemirror editor
nmn Dec 5, 2023
52f2a71
Use rollup and static server.
nmn Dec 5, 2023
9b82f83
Delay preview to wait for built files
nmn Dec 5, 2023
0b46504
chore: update to latest StyleX for playground
nmn Oct 17, 2024
73af4ce
chore: fix build
nmn Oct 21, 2024
e962bf8
chore: playground actually builds
nmn Oct 21, 2024
137ec99
Merge branch 'dev-better-playground' of https://github.com/6ri4n/styl…
6ri4n Oct 30, 2024
8bf65fc
chore: add serve package to dependencies
6ri4n Nov 1, 2024
fbedfe7
feat: adjust build setup to use vite
6ri4n Nov 1, 2024
6eb999a
feat: allow file editing and updates the preview
6ri4n Nov 1, 2024
d557714
update apps/docs/components/Playground.js
6ri4n Nov 1, 2024
9135176
update apps/docs/components/Playground.js
6ri4n Nov 1, 2024
6f4c4eb
update apps/docs/components/Playground.js
6ri4n Nov 1, 2024
c900459
update apps/docs/components/Playground.js
6ri4n Nov 1, 2024
ca2b297
update apps/docs/components/Playground.js
6ri4n Nov 1, 2024
66a0a37
feat: WIP preview with rollup setup
6ri4n Nov 8, 2024
38b0e45
feat: add preview functionality
6ri4n Nov 17, 2024
ed38cec
chore: format App.jsx boilerplate
6ri4n Nov 17, 2024
e9299d7
preview with vite setup
6ri4n Dec 2, 2024
c407df1
feat: WIP failed to load error message and reload preview button
6ri4n Dec 6, 2024
31d1c8a
chore: update package-lock.json
6ri4n Dec 6, 2024
c0885a4
chore: refactor error handling
6ri4n Dec 6, 2024
702c0c1
feat: WIP failed to load error message
6ri4n Dec 6, 2024
2c72e88
feat: WIP failed to load error message
6ri4n Dec 6, 2024
8d2397d
feat: failed to load error message and reload preview button
6ri4n Dec 6, 2024
a9072eb
feat: failed to load error message
6ri4n Dec 7, 2024
3e417f6
chore: add console logs
6ri4n Dec 8, 2024
03aa545
chore: check if iframe exists before reloading preview
6ri4n Dec 8, 2024
9ff4976
chore: add error handling in reloadWebContainer function
6ri4n Dec 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions apps/docs/components/Playground.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* 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 [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 start`...');
const process = await containerInstance.spawn('npm', ['start']);
console.log('Spawned `npm start`...');
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);
});
};

const updateFiles = async () => {
const containerInstance = instance.current;
const filePath = './src/app.jsx';
const updatedCode = code;
await containerInstance.fs.writeFile(filePath, updatedCode);
await wcSpawn(containerInstance, 'node', ['generateCSS.js']);
6ri4n marked this conversation as resolved.
Show resolved Hide resolved
};

const handleCodeChange = (newCode) => {
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
}
6ri4n marked this conversation as resolved.
Show resolved Hide resolved

debounceTimeout.current = setTimeout(async () => {
setCode((prevCode) => newCode);

Check failure on line 98 in apps/docs/components/Playground.js

View workflow job for this annotation

GitHub Actions / lint

'prevCode' is defined but never used. Allowed unused args must match /^_/u
6ri4n marked this conversation as resolved.
Show resolved Hide resolved
if (url) {
try {
await updateFiles();
console.log('Successfully applied changes.');
} catch (err) {
console.error(err);
}
}
}, 3000);
};

useEffect(() => {
require('codemirror/mode/javascript/javascript');
makeWebcontainer().then((i) => {
instance.current = i;
build();
});
() => {
instance.current.unmount();
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
}
};
}, []);

return (
<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)}

Check failure on line 137 in apps/docs/components/Playground.js

View workflow job for this annotation

GitHub Actions / lint

Props should be sorted alphabetically
/>
{url ? (
6ri4n marked this conversation as resolved.
Show resolved Hide resolved
<iframe {...stylex.props(styles.textarea)} src={url} />
) : (
<div {...stylex.props(styles.textarea)}>Loading...</div>
)}
</>
)}
</BrowserOnly>
</div>
);
}

const styles = stylex.create({
container: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: stylex.firstThatWorks('calc(100dvh - 60px)', 'calc(100vh - 60px)'),
borderBottomWidth: 2,
borderBottomStyle: 'solid',
borderBottomColor: 'var(--cyan)',
},
textarea: {
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
justifyContent: 'stretch',
width: '50%',
height: '100%',
borderWidth: 0,
borderStyle: 'none',
},
});
210 changes: 210 additions & 0 deletions apps/docs/components/playground-utils/files.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/**
* 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
*/

export const files = {
'generateCSS.js': {
file: {
contents: `
const fs = require("fs/promises");
const { transformAsync } = require("@babel/core");
const stylexBabelPlugin = require("@stylexjs/babel-plugin");
const flowSyntaxPlugin = require("@babel/plugin-syntax-flow");
const jsxSyntaxPlugin = require("@babel/plugin-syntax-jsx");
const path = require("path");
const { mkdirp } = require("mkdirp");

async function transformFile(filePath) {
const code = await fs.readFile(filePath, "utf8");
const result = await transformAsync(code, {
filename: filePath,
plugins: [
flowSyntaxPlugin,
jsxSyntaxPlugin,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The StyleX plugin should already be doing this.

[
stylexBabelPlugin,
{
dev: false,
test: false,
stylexSheetName: "<>",
genConditionalClasses: true,
unstable_moduleResolution: {
type: "commonJS",
rootDir: path.join(__dirname),
},
},
],
],
sourceType: "unambiguous",
babelrc: false,
});
return result.metadata.stylex;
}

async function getAllFilesOfType(folder, type) {
const contents = await fs.readdir(folder, { withFileTypes: true });

const files = await Promise.all(
contents.map(async (dirent) => {
const subPath = path.join(folder, dirent.name);
if (dirent.isDirectory()) {
return await getAllFilesOfType(subPath, type);
}
if (dirent.name.endsWith(type)) {
return subPath;
}
return null;
})
);

return files.flat().filter(Boolean);
}

async function genSheet() {
const src = await getAllFilesOfType(path.join(__dirname, "src"), ".jsx");
const ruleSets = await Promise.all(src.map(transformFile));
const generatedCSS = stylexBabelPlugin.processStylexRules(ruleSets.flat());
const outputDir = path.join(__dirname, "src");
const cssPath = path.join(outputDir, "stylex.css");

await mkdirp(outputDir);
await fs.writeFile(cssPath, generatedCSS);

console.log("Successfully generated CSS.");
}

genSheet();
`,
},
},
'index.html': {
file: {
contents: `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Stylex Playground</title>
</head>
<body>
<h1 style="color: blue">Loaded HTML from webcontainer!</h1>
<div id="root"></div>
<script type="module" src="./src/main.jsx"></script>
</body>
</html>
`,
},
},
'vite.config.js': {
file: {
contents: `
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
plugins: [
react({
babel: {
presets: ["@babel/preset-react"],
plugins: ["@stylexjs/babel-plugin"],
},
}),
],
server: {
port: 3111,
},
});
`,
},
},
'package.json': {
file: {
contents: `
{
"name": "stylex-playground",
"version": "1.0.0",
"description": "Playground using WebContainers",
"main": "index.js",
"scripts": {
"start": "node generateCSS.js && vite dev"
},
"dependencies": {
"@babel/cli": "latest",
"@babel/core": "latest",
"@babel/plugin-syntax-flow": "latest",
"@babel/plugin-syntax-jsx": "latest",
"@babel/plugin-syntax-typescript": "latest",
"@babel/preset-env": "^7.23.5",
"@babel/preset-react": "^7.23.3",
"@stylexjs/babel-plugin": "^0.8.0",
"@stylexjs/stylex": "^0.8.0",
"babel-plugin-transform-node-env-inline": "^0.4.3",
"react": "*",
"react-dom": "*",
"vite": "^5.4.10",
"@vitejs/plugin-react": "^4.3.3",
"mkdirp": "^3.0.1"
}
}
`,
},
},
src: {
directory: {
'main.jsx': {
file: {
contents: `
import * as React from "react";
import { createRoot } from "react-dom/client";
import Card from "./app.jsx";

const container = document.getElementById("root");
const root = createRoot(container);
root.render(<Card em={true}>Hello World!</Card>);
`,
},
},
'app.jsx': {
file: {
contents: `
import * as React from "react";
import * as stylex from "@stylexjs/stylex";
import "./stylex.css";

export default function Card({ children, em = false, props }) {
return (
<div {...props} {...stylex.props(styles.base, em && styles.emphasise)}>
{children}
</div>
);
}

const styles = stylex.create({
base: {
appearance: "none",
backgroundColor: "blue",
borderRadius: 4,
borderStyle: "none",
boxSize: "border-box",
color: "white",
marginInline: "auto",
paddingBlock: 4,
paddingInline: 8,
width: "95%",
},
emphasise: {
transform: "rotate(-2deg)",
},
});
`,
},
},
},
},
};
Loading
Loading