Spaces:
Running
Running
| import { BlockDefinition, ProjectFile, VisualElement } from '../types/blocks'; | |
| interface CompileOptions { | |
| blocks: BlockDefinition[]; | |
| fileTree: ProjectFile[]; | |
| visualElements: VisualElement[]; | |
| activeFile?: ProjectFile; | |
| projectName: string; | |
| } | |
| export function compileWeb(options: CompileOptions): string { | |
| const { fileTree, activeFile, blocks } = options; | |
| if (!activeFile) { | |
| // Generate default index.html with all visual elements | |
| return generateDefaultHTML(options); | |
| } | |
| switch (activeFile.type) { | |
| case 'html': | |
| return activeFile.content || generateDefaultHTML(options); | |
| case 'css': | |
| return activeFile.content || '/* Styles */\n'; | |
| case 'js': | |
| return activeFile.content || '// JavaScript\n'; | |
| default: | |
| return activeFile.content || ''; | |
| } | |
| } | |
| function findFileInTree(files: ProjectFile[], id: string): ProjectFile | null { | |
| for (const f of files) { | |
| if (f.id === id) return f; | |
| if (f.children) { | |
| const found = findFileInTree(f.children, id); | |
| if (found) return found; | |
| } | |
| } | |
| return null; | |
| } | |
| function generateDefaultHTML(options: CompileOptions): string { | |
| const { visualElements, projectName, fileTree, activeFile } = options; | |
| // Find linked CSS and JS files | |
| const linkedIds = activeFile?.linkedFiles || []; | |
| const linkedFiles = linkedIds.map(id => findFileInTree(fileTree, id)).filter(Boolean) as ProjectFile[]; | |
| const linkedCSS = linkedFiles.filter(f => f.type === 'css'); | |
| const linkedJS = linkedFiles.filter(f => f.type === 'js'); | |
| // Build link and script tags | |
| const cssLinks = linkedCSS.map(f => ` <link rel="stylesheet" href="${f.name}" />`).join('\n'); | |
| const jsScripts = linkedJS.map(f => ` <script src="${f.name}"></script>`).join('\n'); | |
| return `<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>${projectName || 'My Web App'}</title> | |
| ${cssLinks} | |
| </head> | |
| <body> | |
| ${visualElements.map(el => renderVisualElement(el, 0)).join('\n')} | |
| ${jsScripts} | |
| </body> | |
| </html>`; | |
| } | |
| export function renderVisualElement(el: VisualElement, depth: number): string { | |
| const indent = ' '.repeat(depth + 1); | |
| const props = Object.entries(el.props) | |
| .filter(([k, v]) => v !== undefined && v !== null && v !== '' && k !== 'textContent' && k !== 'children') | |
| .map(([k, v]) => { | |
| if (typeof v === 'boolean') return v ? k : ''; | |
| if (k === 'style') return ''; | |
| return `${k}="${String(v).replace(/"/g, '"')}"`; | |
| }) | |
| .filter(Boolean) | |
| .join(' '); | |
| const styles = Object.entries(el.styles) | |
| .map(([k, v]) => `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${v}`) | |
| .join('; '); | |
| const styleAttr = styles ? ` style="${styles}"` : ''; | |
| const classAttr = el.classes.length ? ` class="${el.classes.join(' ')}"` : ''; | |
| const propsStr = props ? ' ' + props : ''; | |
| const attrs = propsStr + styleAttr + classAttr; | |
| const content = el.props.textContent || ''; | |
| if (el.children && el.children.length > 0) { | |
| return `${indent}<${el.tagName}${attrs}> | |
| ${el.children.map(c => renderVisualElement(c, depth + 1)).join('\n')} | |
| ${indent}</${el.tagName}>`; | |
| } | |
| if (el.tagName === 'img' || el.tagName === 'input' || el.tagName === 'br') { | |
| return `${indent}<${el.tagName}${attrs} />`; | |
| } | |
| return `${indent}<${el.tagName}${attrs}>${content || ''}</${el.tagName}>`; | |
| } | |
| export function compileElectron(options: CompileOptions): string { | |
| const { activeFile } = options; | |
| if (!activeFile) return '// Electron main process\n'; | |
| switch (activeFile.type) { | |
| case 'typescript': | |
| return activeFile.content || generateDefaultElectronMain(options); | |
| case 'html': | |
| return activeFile.content || generateDefaultHTML(options); | |
| case 'css': | |
| return activeFile.content || '/* Styles */\n'; | |
| default: | |
| return activeFile.content || ''; | |
| } | |
| } | |
| function generateDefaultElectronMain(options: CompileOptions): string { | |
| return `import { app, BrowserWindow, ipcMain } from 'electron'; | |
| import path from 'path'; | |
| let mainWindow: BrowserWindow | null = null; | |
| function createWindow(): void { | |
| mainWindow = new BrowserWindow({ | |
| width: 1200, | |
| height: 800, | |
| webPreferences: { | |
| preload: path.join(__dirname, 'preload.js'), | |
| nodeIntegration: false, | |
| contextIsolation: true, | |
| }, | |
| }); | |
| mainWindow.loadFile('index.html'); | |
| mainWindow.on('closed', () => { mainWindow = null; }); | |
| } | |
| app.whenReady().then(() => { | |
| createWindow(); | |
| app.on('activate', () => { | |
| if (BrowserWindow.getAllWindows().length === 0) { | |
| createWindow(); | |
| } | |
| }); | |
| }); | |
| app.on('window-all-closed', () => { | |
| if (process.platform !== 'darwin') { | |
| app.quit(); | |
| } | |
| }); | |
| `; | |
| } | |
| export function compileMaui(options: CompileOptions): string { | |
| const { activeFile } = options; | |
| if (!activeFile) return '<!-- .NET MAUI Page -->\n'; | |
| switch (activeFile.type) { | |
| case 'xaml': | |
| return activeFile.content || generateDefaultXaml(options); | |
| case 'csharp': | |
| return activeFile.content || generateDefaultCSharp(options); | |
| default: | |
| return activeFile.content || ''; | |
| } | |
| } | |
| function generateDefaultXaml(options: CompileOptions): string { | |
| return `<?xml version="1.0" encoding="utf-8" ?> | |
| <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" | |
| xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | |
| x:Class="${options.projectName}.MainPage" | |
| Title="Main Page" | |
| BackgroundColor="White"> | |
| <VerticalStackLayout Spacing="16" Padding="24"> | |
| <Label Text="Welcome to ${options.projectName}!" | |
| FontSize="24" | |
| FontAttributes="Bold" | |
| HorizontalTextAlignment="Center" /> | |
| <Entry x:Name="inputField" | |
| Placeholder="Enter text here..." /> | |
| <Button x:Name="submitButton" | |
| Text="Submit" | |
| BackgroundColor="Blue" | |
| TextColor="White" | |
| Clicked="OnSubmitClicked" /> | |
| </VerticalStackLayout> | |
| </ContentPage>`; | |
| } | |
| function generateDefaultCSharp(options: CompileOptions): string { | |
| return `namespace ${options.projectName}; | |
| public partial class MainPage : ContentPage | |
| { | |
| public MainPage() | |
| { | |
| InitializeComponent(); | |
| } | |
| private void OnSubmitClicked(object sender, EventArgs e) | |
| { | |
| DisplayAlert("Hello", $"You entered: {inputField.Text}", "OK"); | |
| } | |
| }`; | |
| } | |
| export function compileNodeJS(options: CompileOptions): string { | |
| const { activeFile } = options; | |
| if (!activeFile) return '// Node.js server\n'; | |
| switch (activeFile.type) { | |
| case 'js': | |
| return activeFile.content || generateDefaultExpress(options); | |
| case 'json': | |
| return activeFile.content || generateDefaultPackageJson(options); | |
| case 'env': | |
| return activeFile.content || '# Environment Variables\nPORT=3000\n'; | |
| default: | |
| return activeFile.content || ''; | |
| } | |
| } | |
| function generateDefaultExpress(options: CompileOptions): string { | |
| return `const express = require('express'); | |
| const cors = require('cors'); | |
| const app = express(); | |
| const PORT = process.env.PORT || 3000; | |
| // Middleware | |
| app.use(cors()); | |
| app.use(express.json()); | |
| app.use(express.urlencoded({ extended: true })); | |
| // Routes | |
| app.get('/', (req, res) => { | |
| res.json({ | |
| message: 'Welcome to ${options.projectName}!', | |
| version: '1.0.0', | |
| }); | |
| }); | |
| // Start server | |
| app.listen(PORT, () => { | |
| console.log(\`Server running on port \${PORT}\`); | |
| }); | |
| `; | |
| } | |
| /** | |
| * Generate a complete HTML document for preview purposes. | |
| * Renders visual elements and inlines linked CSS content. | |
| * Does NOT include script tags — the caller injects those. | |
| */ | |
| export function generatePreviewHtml(options: { | |
| visualElements: VisualElement[]; | |
| projectName: string; | |
| linkedCssContent: { content: string }[]; | |
| }): string { | |
| const { visualElements, projectName, linkedCssContent } = options; | |
| const cssBlocks = linkedCssContent | |
| .filter(f => f.content) | |
| .map(f => f.content) | |
| .join('\n\n'); | |
| const styleTag = cssBlocks | |
| ? ` <style>\n${cssBlocks}\n </style>` | |
| : ''; | |
| return `<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>${projectName || 'My Web App'}</title> | |
| ${styleTag} | |
| </head> | |
| <body> | |
| ${visualElements.map(el => renderVisualElement(el, 0)).join('\n')} | |
| </body> | |
| </html>`; | |
| } | |
| function generateDefaultPackageJson(options: CompileOptions): string { | |
| return JSON.stringify({ | |
| name: options.projectName.toLowerCase().replace(/\s+/g, '-'), | |
| version: '1.0.0', | |
| description: `${options.projectName} - built with RealBlocks`, | |
| main: 'server.js', | |
| scripts: { | |
| start: 'node server.js', | |
| dev: 'nodemon server.js', | |
| }, | |
| dependencies: { | |
| express: '^4.18.2', | |
| cors: '^2.8.5', | |
| }, | |
| devDependencies: { | |
| nodemon: '^3.0.0', | |
| }, | |
| }, null, 2); | |
| } | |