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:
KevIsDev
2025-04-15 15:32:40 +01:00
parent 2f09d512bc
commit 63129a93cd
6 changed files with 405 additions and 81 deletions

View File

@@ -565,6 +565,12 @@ export const Preview = memo(() => {
}
};
const openInNewTab = () => {
if (activePreview?.baseUrl) {
window.open(activePreview?.baseUrl, '_blank');
}
};
// Function to get the correct frame padding based on orientation
const getFramePadding = useCallback(() => {
if (!selectedWindowSize) {
@@ -767,8 +773,17 @@ export const Preview = memo(() => {
<span className="text-sm font-medium text-[#111827] dark:text-gray-300">Device Options</span>
</div>
<div className="flex flex-col gap-2">
<button
className={`flex w-full justify-between items-center text-start bg-transparent text-xs text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary`}
onClick={() => {
openInNewTab();
}}
>
<span>Open in new tab</span>
<div className='i-ph:arrow-square-out h-5 w-4'/>
</button>
<div className="flex items-center justify-between">
<span className="text-xs text-[#6B7280] dark:text-gray-400">Show Device Frame</span>
<span className="text-xs text-bolt-elements-textTertiary">Show Device Frame</span>
<button
className={`w-10 h-5 rounded-full transition-colors duration-200 ${
showDeviceFrame ? 'bg-[#6D28D9]' : 'bg-gray-300 dark:bg-gray-700'
@@ -786,7 +801,7 @@ export const Preview = memo(() => {
</button>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-[#6B7280] dark:text-gray-400">Landscape Mode</span>
<span className="text-xs text-bolt-elements-textTertiary">Landscape Mode</span>
<button
className={`w-10 h-5 rounded-full transition-colors duration-200 ${
isLandscape ? 'bg-[#6D28D9]' : 'bg-gray-300 dark:bg-gray-700'

View File

@@ -476,6 +476,238 @@ Here are some examples of correct usage of artifacts:
</assistant_response>
</example>
</examples>
<mobile_app_instructions>
The following instructions guide how you should handle mobile app development using Expo and React Native.
IMPORTANT: These instructions should only be used for mobile app development if the users requests it.
<core_requirements>
- Version: 2025
- Platform: Web-first with mobile compatibility
- Expo SDK: 52.0.30
- Expo Router: 4.0.20
- Type: Expo Managed Workflow
- You have been given an example package.json below you dont need to add every dependency to the package.json only the required dependencies and never add any unnecessary dependencies for the project at hand.
<design_philosophy>
- All designs MUST be beautiful and professional, not cookie cutter
- Apps MUST be fully featured and production-ready
- Implement unique, thoughtful user experiences
- Focus on clean, maintainable code structure
- Every component must be properly typed with TypeScript
- All UI must be responsive and work across all screen sizes
</design_philosophy>
</core_requirements>
<project_structure>
/app # All routes must be here
├── _layout.tsx # Root layout (required)
├── +not-found.tsx # 404 handler
└── (tabs)/ # Tab-based navigation group
├── _layout.tsx # Tab configuration
└── [tab].tsx # Individual tab screens
/hooks # Custom hooks
/types # TypeScript type definitions
/assets # Static assets (images, etc.)
</project_structure>
<critical_requirements>
<framework_setup>
- MUST preserve useFrameworkReady hook in app/_layout.tsx
- MUST maintain existing dependencies
- NO native code files (ios/android directories)
- NEVER modify the useFrameworkReady hook
- ALWAYS maintain the exact structure of _layout.tsx
</framework_setup>
<component_requirements>
- Every component must have proper TypeScript types
- All props must be explicitly typed
- Use proper React.FC typing for functional components
- Implement proper loading and error states
- Handle edge cases and empty states
</component_requirements>
<navigation_architecture>
<primary_navigation>
- Tab-based Navigation via expo-router
- Main sections accessible through tabs
</primary_navigation>
<secondary_navigation>
- Stack Navigation: For hierarchical flows
- Modal Navigation: For overlays
- Drawer Navigation: For additional menus
</secondary_navigation>
</navigation_architecture>
<styling_guidelines>
- Use StyleSheet.create exclusively
- NO NativeWind or alternative styling libraries
- Maintain consistent spacing and typography
- Follow 8-point grid system for spacing
- Use platform-specific shadows
- Implement proper dark mode support
- Handle safe area insets correctly
- Support dynamic text sizes
</styling_guidelines>
<font_management>
- Use @expo-google-fonts packages only
- NO local font files
- Implement proper font loading with SplashScreen
- Handle loading states appropriately
- Load fonts at root level
- Provide fallback fonts
- Handle font scaling
</font_management>
<icons>
Library: lucide-react-native
Default Props:
- size: 24
- color: 'currentColor'
- strokeWidth: 2
- absoluteStrokeWidth: false
</icons>
<image_handling>
- Use Unsplash for stock photos
- Direct URL linking only
- ONLY use valid, existing Unsplash URLs
- NO downloading or storing of images locally
- Proper Image component implementation
- Test all image URLs to ensure they load correctly
- Implement proper loading states
- Handle image errors gracefully
- Use appropriate image sizes
- Implement lazy loading where appropriate
</image_handling>
<error_handling>
- Display errors inline in UI
- NO Alert API usage
- Implement error states in components
- Handle network errors gracefully
- Provide user-friendly error messages
- Implement retry mechanisms where appropriate
- Log errors for debugging
- Handle edge cases appropriately
- Provide fallback UI for errors
</error_handling>
<environment_variables>
- Use Expo's env system
- NO Vite env variables
- Proper typing in env.d.ts
- Handle missing variables gracefully
- Validate environment variables at startup
- Use proper naming conventions (EXPO_PUBLIC_*)
</environment_variables>
<platform_compatibility>
- Check platform compatibility
- Use Platform.select() for specific code
- Implement web alternatives for native-only features
- Handle keyboard behavior differently per platform
- Implement proper scrolling behavior for web
- Handle touch events appropriately per platform
- Support both mouse and touch input on web
- Handle platform-specific styling
- Implement proper focus management
</platform_compatibility>
<api_routes>
Location: app/[route]+api.ts
Features:
- Secure server code
- Custom endpoints
- Request/Response handling
- Error management
- Proper validation
- Rate limiting
- CORS handling
- Security headers
</api_routes>
<animation_libraries>
Preferred:
- react-native-reanimated over Animated
- react-native-gesture-handler over PanResponder
</animation_libraries>
<performance_optimization>
- Implement proper list virtualization
- Use memo and useCallback appropriately
- Optimize re-renders
- Implement proper image caching
- Handle memory management
- Clean up resources properly
- Implement proper error boundaries
- Use proper loading states
- Handle offline functionality
- Implement proper data caching
</performance_optimization>
<security_best_practices>
- Implement proper authentication
- Handle sensitive data securely
- Validate all user input
- Implement proper session management
- Use secure storage for sensitive data
- Implement proper CORS policies
- Handle API keys securely
- Implement proper error handling
- Use proper security headers
- Handle permissions properly
</security_best_practices>
<accessibility>
- Implement proper ARIA labels
- Support screen readers
- Handle focus management
- Provide proper contrast
- Support keyboard navigation
- Handle reduced motion
- Implement proper semantic markup
- Support dynamic text sizing
- Handle color blindness
- Provide alternative text
</accessibility>
</critical_requirements>
<development_workflow>
1. Start with root layout
2. Implement navigation structure
3. Create reusable components
4. Add proper error handling
5. Implement platform-specific code
6. Add proper loading states
7. Test cross-platform compatibility
8. Implement proper security
9. Add proper accessibility
10. Optimize performance
</development_workflow>
<common_pitfalls>
- Removing useFrameworkReady hook
- Using native-only APIs without web alternatives
- Improper font loading implementation
- Missing platform-specific checks
- Not handling keyboard behavior properly
- Improper safe area handling
- Missing loading states
- Poor error handling
- Incomplete type definitions
- Not cleaning up resources properly
- Improper memory management
- Missing accessibility features
- Poor performance optimization
- Insecure data handling
- Missing validation
- Improper error boundaries
</common_pitfalls>
</mobile_app_instructions>
`;
export const CONTINUE_PROMPT = stripIndents`

View 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 });
}
}

View 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' },
});
};

View File

@@ -25,11 +25,19 @@ PROVIDER_LIST.forEach((provider) => {
// starter Templates
export const STARTER_TEMPLATES: Template[] = [
{
name: 'bolt-expo-app',
label: 'Expo App',
description: 'Expo starter template for building cross-platform mobile apps',
githubRepo: 'xKevIsDev/bolt-expo-template',
tags: ['mobile', 'expo', 'mobile-app', 'mobile app', 'android', 'iphone'],
icon: 'i-bolt:expo',
},
{
name: 'bolt-astro-basic',
label: 'Astro Basic',
description: 'Lightweight Astro starter template for building fast static websites',
githubRepo: 'thecodacus/bolt-astro-basic-template',
githubRepo: 'xKevIsDev/bolt-astro-basic-template',
tags: ['astro', 'blog', 'performance'],
icon: 'i-bolt:astro',
},
@@ -37,7 +45,7 @@ export const STARTER_TEMPLATES: Template[] = [
name: 'bolt-nextjs-shadcn',
label: 'Next.js with shadcn/ui',
description: 'Next.js starter fullstack template integrated with shadcn/ui components and styling system',
githubRepo: 'thecodacus/bolt-nextjs-shadcn-template',
githubRepo: 'xKevIsDev/bolt-nextjs-shadcn-template',
tags: ['nextjs', 'react', 'typescript', 'shadcn', 'tailwind'],
icon: 'i-bolt:nextjs',
},
@@ -45,7 +53,7 @@ export const STARTER_TEMPLATES: Template[] = [
name: 'bolt-qwik-ts',
label: 'Qwik TypeScript',
description: 'Qwik framework starter with TypeScript for building resumable applications',
githubRepo: 'thecodacus/bolt-qwik-ts-template',
githubRepo: 'xKevIsDev/bolt-qwik-ts-template',
tags: ['qwik', 'typescript', 'performance', 'resumable'],
icon: 'i-bolt:qwik',
},
@@ -53,7 +61,7 @@ export const STARTER_TEMPLATES: Template[] = [
name: 'bolt-remix-ts',
label: 'Remix TypeScript',
description: 'Remix framework starter with TypeScript for full-stack web applications',
githubRepo: 'thecodacus/bolt-remix-ts-template',
githubRepo: 'xKevIsDev/bolt-remix-ts-template',
tags: ['remix', 'typescript', 'fullstack', 'react'],
icon: 'i-bolt:remix',
},
@@ -61,7 +69,7 @@ export const STARTER_TEMPLATES: Template[] = [
name: 'bolt-slidev',
label: 'Slidev Presentation',
description: 'Slidev starter template for creating developer-friendly presentations using Markdown',
githubRepo: 'thecodacus/bolt-slidev-template',
githubRepo: 'xKevIsDev/bolt-slidev-template',
tags: ['slidev', 'presentation', 'markdown'],
icon: 'i-bolt:slidev',
},
@@ -77,7 +85,7 @@ export const STARTER_TEMPLATES: Template[] = [
name: 'vanilla-vite',
label: 'Vanilla + Vite',
description: 'Minimal Vite starter template for vanilla JavaScript projects',
githubRepo: 'thecodacus/vanilla-vite-template',
githubRepo: 'xKevIsDev/vanilla-vite-template',
tags: ['vite', 'vanilla-js', 'minimal'],
icon: 'i-bolt:vite',
},
@@ -85,7 +93,7 @@ export const STARTER_TEMPLATES: Template[] = [
name: 'bolt-vite-react',
label: 'React + Vite + typescript',
description: 'React starter template powered by Vite for fast development experience',
githubRepo: 'thecodacus/bolt-vite-react-ts-template',
githubRepo: 'xKevIsDev/bolt-vite-react-ts-template',
tags: ['react', 'vite', 'frontend'],
icon: 'i-bolt:react',
},
@@ -93,7 +101,7 @@ export const STARTER_TEMPLATES: Template[] = [
name: 'bolt-vite-ts',
label: 'Vite + TypeScript',
description: 'Vite starter template with TypeScript configuration for type-safe development',
githubRepo: 'thecodacus/bolt-vite-ts-template',
githubRepo: 'xKevIsDev/bolt-vite-ts-template',
tags: ['vite', 'typescript', 'minimal'],
icon: 'i-bolt:typescript',
},
@@ -101,7 +109,7 @@ export const STARTER_TEMPLATES: Template[] = [
name: 'bolt-vue',
label: 'Vue.js',
description: 'Vue.js starter template with modern tooling and best practices',
githubRepo: 'thecodacus/bolt-vue-template',
githubRepo: 'xKevIsDev/bolt-vue-template',
tags: ['vue', 'typescript', 'frontend'],
icon: 'i-bolt:vue',
},
@@ -109,7 +117,7 @@ export const STARTER_TEMPLATES: Template[] = [
name: 'bolt-angular',
label: 'Angular Starter',
description: 'A modern Angular starter template with TypeScript support and best practices configuration',
githubRepo: 'thecodacus/bolt-angular-template',
githubRepo: 'xKevIsDev/bolt-angular-template',
tags: ['angular', 'typescript', 'frontend', 'spa'],
icon: 'i-bolt:angular',
},

View File

@@ -115,77 +115,19 @@ const getGitHubRepoContent = async (
repoName: string,
path: string = '',
): Promise<{ name: string; path: string; content: string }[]> => {
const baseUrl = 'https://api.github.com';
try {
const token = Cookies.get('githubToken') || import.meta.env.VITE_GITHUB_ACCESS_TOKEN;
const headers: HeadersInit = {
Accept: 'application/vnd.github.v3+json',
};
// Add your GitHub token if needed
if (token) {
headers.Authorization = 'Bearer ' + token;
}
// Fetch contents of the path
const response = await fetch(`${baseUrl}/repos/${repoName}/contents/${path}`, {
headers,
});
// Instead of directly fetching from GitHub, use our own API endpoint as a proxy
const response = await fetch(`/api/github-template?repo=${encodeURIComponent(repoName)}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: any = await response.json();
// If it's a single file, return its content
if (!Array.isArray(data)) {
if (data.type === 'file') {
// If it's a file, get its content
const content = atob(data.content); // Decode base64 content
return [
{
name: data.name,
path: data.path,
content,
},
];
}
}
// Process directory contents recursively
const contents = await Promise.all(
data.map(async (item: any) => {
if (item.type === 'dir') {
// Recursively get contents of subdirectories
return await getGitHubRepoContent(repoName, item.path);
} else if (item.type === 'file') {
// Fetch file content
const fileResponse = await fetch(item.url, {
headers,
});
const fileData: any = await fileResponse.json();
const content = atob(fileData.content); // Decode base64 content
return [
{
name: item.name,
path: item.path,
content,
},
];
}
return [];
}),
);
// Flatten the array of contents
return contents.flat();
// Our API will return the files in the format we need
const files = await response.json() as any;
return files;
} catch (error) {
console.error('Error fetching repo contents:', error);
console.error('Error fetching release contents:', error);
throw error;
}
};
@@ -209,8 +151,9 @@ export async function getTemplates(templateName: string, title?: string) {
filteredFiles = filteredFiles.filter((x) => x.path.startsWith('.git') == false);
// exclude lock files
const comminLockFiles = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
filteredFiles = filteredFiles.filter((x) => comminLockFiles.includes(x.name) == false);
// WE NOW INCLUDE LOCK FILES FOR IMPROVED INSTALL TIMES
{/*const comminLockFiles = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
filteredFiles = filteredFiles.filter((x) => comminLockFiles.includes(x.name) == false);*/}
// exclude .bolt
filteredFiles = filteredFiles.filter((x) => x.path.startsWith('.bolt') == false);
@@ -255,7 +198,6 @@ ${file.content}
TEMPLATE INSTRUCTIONS:
${templatePromptFile.content}
IMPORTANT: Dont Forget to install the dependencies before running the app
---
`;
}
@@ -296,6 +238,8 @@ edit only the files that need to be changed, and you can create new files as nee
NO NOT EDIT/WRITE ANY FILES THAT ALREADY EXIST IN THE PROJECT AND DOES NOT NEED TO BE MODIFIED
---
Now that the Template is imported please continue with my original request
IMPORTANT: Dont Forget to install the dependencies before running the app by using \`npm install && npm run dev\`
`;
return {