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

[core] POC: online @toolpad/core project generator #3807

Draft
wants to merge 30 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dd0abaf
webcontainers to run generated app on docs
Janpot Jul 19, 2024
57376cd
Update builder.tsx
Janpot Jul 19, 2024
e73dd62
Merge remote-tracking branch 'upstream/master' into web-demo
Janpot Jul 19, 2024
77a5abe
fixes
Janpot Jul 19, 2024
ff8fea6
WIP
Janpot Jul 19, 2024
385608f
Update
Janpot Jul 19, 2024
9362d20
Update builder.tsx
Janpot Jul 19, 2024
84c395b
Update builder.tsx
Janpot Jul 19, 2024
9725842
ewr
Janpot Jul 19, 2024
e96540c
Update find.js
Janpot Jul 19, 2024
9e1ab05
Update find.js
Janpot Jul 19, 2024
5446913
Merge branch 'master' into web-demo
Janpot Jul 19, 2024
7b71658
Update builder.tsx
Janpot Jul 19, 2024
3071872
Update builder.tsx
Janpot Jul 19, 2024
6a6ca39
Merge branch 'master' into web-demo
Janpot Jul 19, 2024
b927d4c
chrome
Janpot Jul 20, 2024
4e4824d
Merge remote-tracking branch 'upstream/master' into web-demo
Janpot Jul 20, 2024
6a5ed2a
Update CorePlayground.tsx
Janpot Jul 20, 2024
65798aa
dewe
Janpot Jul 20, 2024
3b417c0
wferfwrf
Janpot Jul 20, 2024
0b2a1a7
dewwe
Janpot Jul 20, 2024
ad10749
Code viewer
Janpot Jul 20, 2024
82c24b7
dew
Janpot Jul 20, 2024
a51e6d5
Update tsconfig.json
Janpot Jul 20, 2024
9e9c74d
Update CodeViewer.tsx
Janpot Jul 20, 2024
64582b9
Update generateProject.ts
Janpot Jul 20, 2024
7d1d0ee
csbci
Janpot Jul 20, 2024
0b119a0
Merge remote-tracking branch 'upstream/master' into web-demo
Janpot Jul 22, 2024
72729df
Merge branch 'master' into web-demo
Janpot Jul 24, 2024
6394487
Merge branch 'master' into web-demo
Janpot Jul 24, 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
20 changes: 20 additions & 0 deletions docs/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import * as path from 'path';
import * as url from 'url';
import { createRequire } from 'module';
import { headers } from 'next/headers.js';
import { LANGUAGES, LANGUAGES_IGNORE_PAGES, LANGUAGES_IN_PROGRESS } from './config.js';

const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url));
Expand Down Expand Up @@ -74,6 +75,10 @@ export default withDocsInfra({
'../packages/toolpad-core/package.json',
),
'@toolpad/core': path.resolve(currentDirectory, '../packages/toolpad-core/src'),
'create-toolpad-app': path.resolve(
currentDirectory,
'../packages/create-toolpad-app/src/api.ts',
),
},
},
module: {
Expand Down Expand Up @@ -168,5 +173,20 @@ export default withDocsInfra({
permanent: false,
},
],
headers: async () => [
{
source: '/toolpad/core/builder',
headers: [
{
key: 'Cross-Origin-Embedder-Policy',
value: 'require-corp',
},
{
key: 'Cross-Origin-Opener-Policy',
value: 'same-origin',
},
],
},
],
}),
});
3 changes: 3 additions & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@
"@mui/x-date-pickers": "7.11.0",
"@mui/x-date-pickers-pro": "7.11.0",
"@mui/x-license": "7.11.0",
"@mui/x-tree-view": "7.11.0",
"@toolpad/core": "workspace:*",
"@toolpad/studio": "workspace:*",
"@trendmicro/react-interpolate": "0.5.5",
"@types/lodash": "4.17.6",
"@types/react-router-dom": "5.3.3",
"@webcontainer/api": "1.3.0-internal.1",
"ast-types": "0.14.2",
"autoprefixer": "10.4.19",
"babel-plugin-module-resolver": "5.0.2",
Expand All @@ -54,6 +56,7 @@
"clipboard-copy": "4.0.1",
"clsx": "2.1.1",
"core-js": "2.6.12",
"create-toolpad-app": "workspace:*",
"cross-env": "7.0.3",
"date-fns-jalali": "2.29.3-0",
"dayjs": "1.11.11",
Expand Down
39 changes: 39 additions & 0 deletions docs/pages/toolpad/core/builder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as React from 'react';
import NoSsr from '@mui/material/NoSsr';
import Head from 'docs/src/modules/components/Head';
import CssBaseline from '@mui/material/CssBaseline';
import BrandingCssVarsProvider from 'docs/src/BrandingCssVarsProvider';
import AppHeader from 'docs/src/layouts/AppHeader';
import AppHeaderBanner from 'docs/src/components/banner/AppHeaderBanner';
import GlobalStyles from '@mui/material/GlobalStyles';
import { Box, styled } from '@mui/material';
import CorePlayground from '../../../src/components/builder/CorePlayground';
import SignUpToast from '../../../src/components/landing/SignUpToast';

const Main = styled('main')({
flex: 1,
});

export default function Builder() {
return (
<BrandingCssVarsProvider>
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Head
title="Toolpad: Low-code admin builder"
description="Build apps with Material UI components, connect to data sources, APIs and build your internal tools 10x faster. Open-source and powered by MUI."
card="/static/toolpad/marketing/toolpad-og.jpg"
/>
<NoSsr>
<SignUpToast />
</NoSsr>
<CssBaseline />
<GlobalStyles styles={{ 'html, body, #__next': { width: '100%', height: '100%' } }} />
<AppHeaderBanner />
<AppHeader gitHubRepository="https://github.com/mui/mui-toolpad" />
<Main id="main-content">
<CorePlayground />
</Main>
</Box>
</BrandingCssVarsProvider>
);
}
5 changes: 5 additions & 0 deletions docs/public/_headers
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
/favicon.ico
Content-Type: image/x-icon

/toolpad/core/builder
# https://webcontainers.io/guides/configuring-headers
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

/*
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# Block usage in iframes.
Expand Down
204 changes: 204 additions & 0 deletions docs/src/components/builder/AppViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import * as React from 'react';
import { WebContainer } from '@webcontainer/api';
import { Box, CircularProgress, SxProps, Typography, styled } from '@mui/material';
import { Files } from 'create-toolpad-app';

const Root = styled('div')({
width: '100%',
height: '100%',
position: 'relative',
});

const AppFrame = styled('iframe')(({ theme }) => ({
border: `1px solid ${theme.vars.palette.divider}`,
borderRadius: theme.vars.shape.borderRadius,
overflow: 'hidden',
width: '100%',
height: '100%',
}));

function ensureFolder(folders: string[], containingFolder: Record<string, any>) {
if (folders.length <= 0) {
return containingFolder;
}
const [first, ...rest] = folders;
let folder = containingFolder[first];
if (!folder) {
folder = { directory: {} };
containingFolder[first] = folder;
}
return ensureFolder(rest, folder.directory);
}

type WebcontainerFolder = Record<
string,
| { directory: WebcontainerFolder; file?: undefined }
| { directory?: undefined; file: { contents: string } }
>;

// TODO: generate types for create-toolpad-app API
function createWebcontainerFiles(flatFiles: Files): WebcontainerFolder {
const files: WebcontainerFolder = {};
for (const [name, { content }] of flatFiles) {
const segments = name.split('/');
const folders = segments.slice(0, segments.length - 1);
const folder = ensureFolder(folders, files);
const file = segments[segments.length - 1];
folder[file] = { file: { contents: content } };
}
return files;
}

async function installDependencies(instance: WebContainer) {
// Install dependencies
const installProcess = await instance.spawn('npm', ['install', '--force']);
installProcess.output.pipeTo(
new WritableStream({
write(data) {
// eslint-disable-next-line no-console
console.log(data);
},
}),
);
// Wait for install command to exit
return installProcess.exit;
}

export interface AppViewerProps {
sx?: SxProps;
files?: Files;
}

export default function AppViewer({ sx, files = new Map() }: AppViewerProps) {
const frameRef = React.useRef<HTMLIFrameElement>(null);
const webcontainerPromiseRef = React.useRef<Promise<WebContainer> | null>(null);
const [loading, setLoading] = React.useState(true);
const [rebuilding, setRebuilding] = React.useState(false);
const isRebuildingRef = React.useRef(rebuilding);
React.useEffect(() => {
isRebuildingRef.current = rebuilding;
}, [rebuilding]);

const webcontainerFiles = React.useMemo(() => createWebcontainerFiles(files), [files]);

const bootFilesref = React.useRef(webcontainerFiles);
React.useEffect(() => {
bootFilesref.current = webcontainerFiles;
}, [webcontainerFiles]);

React.useEffect(() => {
if (!frameRef.current) {
throw new Error('Frame not found');
}

const frame = frameRef.current;

const webcontainerPromise = Promise.resolve(webcontainerPromiseRef.current).then(async () =>
WebContainer.boot(),
);
setLoading(true);

webcontainerPromiseRef.current = webcontainerPromise;

webcontainerPromise.then(async (instance) => {
if (webcontainerPromiseRef.current !== webcontainerPromise) {
return;
}

await instance.mount(bootFilesref.current);

const exitCode = await installDependencies(instance);
if (exitCode !== 0) {
throw new Error('Installation failed');
}

// Run `npm run dev` to start the next.js dev server
const devProcess = await instance.spawn('npm', ['run', 'dev']);

devProcess.output.pipeTo(
new WritableStream({
write(data) {
// eslint-disable-next-line no-console
console.log(data);
if (data.includes('Compiled /page')) {
setLoading(false);
}
if (isRebuildingRef.current && data.includes('Compiled in')) {
setRebuilding(false);
}
},
}),
);

// Wait for `server-ready` event
instance.on('server-ready', (port, url) => {
frame.src = `${url}/page`;
});
});

return () => {
if (!webcontainerPromiseRef.current) {
return;
}
webcontainerPromiseRef.current.then((instance) => {
instance.teardown();
});
};
}, []);

const prevFiles = React.useRef(files);
React.useEffect(() => {
const changes = new Map();

for (const [name, { content }] of files) {
if (prevFiles.current.get(name)?.content !== content) {
changes.set(name, { content });
}
}

prevFiles.current = files;

if (changes.size <= 0) {
return;
}

Promise.resolve(webcontainerPromiseRef.current).then(async (instance) => {
if (!instance) {
throw new Error('Instance not found');
}

for (const [name, { content }] of changes) {
instance.fs.writeFile(name, content);
}

setRebuilding(true);
});
}, [files]);

return (
<Root sx={sx}>
{loading || rebuilding ? (
<Box
sx={{
position: 'absolute',
inset: '0 0 0 0',
display: 'flex',
flexDirection: 'column',
gap: 2,
alignItems: 'center',
justifyContent: 'center',
}}
>
<CircularProgress />
<Typography>{'// TODO: show progress. (check the console for now)'}</Typography>
</Box>
) : null}

<AppFrame
ref={frameRef}
title="Application"
style={{ display: 'block', width: '100%', height: '100%' }}
/>
</Root>
);
}
Loading
Loading