mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
feat: add webcontainer connect route and new preview functionality
- Add new route `webcontainer.connect.$id.tsx` for WebContainer connection - Implement `openInNewTab` function in `Preview.tsx` for opening previews in new tabs - Update GitHub template fetching logic to include lock files for improved install times - Add new Expo starter template to constants - Extend prompts with mobile app development instructions -Templates now use Releases from github as a work around for rate limits
This commit is contained in:
93
app/routes/api.github-template.ts
Normal file
93
app/routes/api.github-template.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { json } from '@remix-run/cloudflare';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export async function loader({ request }: { request: Request }) {
|
||||
const url = new URL(request.url);
|
||||
const repo = url.searchParams.get('repo');
|
||||
|
||||
if (!repo) {
|
||||
return json({ error: 'Repository name is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = 'https://api.github.com';
|
||||
|
||||
// Get the latest release
|
||||
const releaseResponse = await fetch(`${baseUrl}/repos/${repo}/releases/latest`, {
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
// Add GitHub token if available in environment variables
|
||||
...(process.env.GITHUB_TOKEN ? { 'Authorization': `Bearer ${process.env.GITHUB_TOKEN}` } : {})
|
||||
}
|
||||
});
|
||||
|
||||
if (!releaseResponse.ok) {
|
||||
throw new Error(`GitHub API error: ${releaseResponse.status}`);
|
||||
}
|
||||
|
||||
const releaseData = await releaseResponse.json() as any;
|
||||
const zipballUrl = releaseData.zipball_url;
|
||||
|
||||
// Fetch the zipball
|
||||
const zipResponse = await fetch(zipballUrl, {
|
||||
headers: {
|
||||
...(process.env.GITHUB_TOKEN ? { 'Authorization': `Bearer ${process.env.GITHUB_TOKEN}` } : {})
|
||||
}
|
||||
});
|
||||
|
||||
if (!zipResponse.ok) {
|
||||
throw new Error(`Failed to fetch release zipball: ${zipResponse.status}`);
|
||||
}
|
||||
|
||||
// Get the zip content as ArrayBuffer
|
||||
const zipArrayBuffer = await zipResponse.arrayBuffer();
|
||||
|
||||
// Use JSZip to extract the contents
|
||||
const zip = await JSZip.loadAsync(zipArrayBuffer);
|
||||
|
||||
// Process the zip contents
|
||||
const files: { name: string; path: string; content: string }[] = [];
|
||||
|
||||
// Find the root folder name
|
||||
let rootFolderName = '';
|
||||
zip.forEach((relativePath, zipEntry) => {
|
||||
if (!rootFolderName && relativePath.includes('/')) {
|
||||
rootFolderName = relativePath.split('/')[0];
|
||||
}
|
||||
});
|
||||
|
||||
// Extract all files
|
||||
const promises = Object.keys(zip.files).map(async (filename) => {
|
||||
const zipEntry = zip.files[filename];
|
||||
|
||||
// Skip directories
|
||||
if (zipEntry.dir) return null;
|
||||
|
||||
// Skip the root folder itself
|
||||
if (filename === rootFolderName) return null;
|
||||
|
||||
// Remove the root folder from the path
|
||||
let normalizedPath = filename;
|
||||
if (rootFolderName && filename.startsWith(rootFolderName + '/')) {
|
||||
normalizedPath = filename.substring(rootFolderName.length + 1);
|
||||
}
|
||||
|
||||
// Get the file content
|
||||
const content = await zipEntry.async('string');
|
||||
|
||||
return {
|
||||
name: normalizedPath.split('/').pop() || '',
|
||||
path: normalizedPath,
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const fileList = results.filter(Boolean) as { name: string; path: string; content: string }[];
|
||||
|
||||
return json(fileList);
|
||||
} catch (error) {
|
||||
console.error('Error processing GitHub template:', error);
|
||||
return json({ error: 'Failed to fetch template files' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
32
app/routes/webcontainer.connect.$id.tsx
Normal file
32
app/routes/webcontainer.connect.$id.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { type LoaderFunction } from '@remix-run/cloudflare';
|
||||
|
||||
export const loader: LoaderFunction = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const editorOrigin = url.searchParams.get('editorOrigin') || 'https://stackblitz.com';
|
||||
console.log('editorOrigin', editorOrigin);
|
||||
|
||||
const htmlContent = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Connect to WebContainer</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module">
|
||||
(async () => {
|
||||
const { setupConnect } = await import('https://cdn.jsdelivr.net/npm/@webcontainer/api@latest/dist/connect.js');
|
||||
setupConnect({
|
||||
editorOrigin: '${editorOrigin}'
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
return new Response(htmlContent, {
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user