From 956b4838ed3558c5e00cad30626c6009b4562560 Mon Sep 17 00:00:00 2001 From: ZAKARYA EL BAZY Date: Fri, 23 Jan 2026 10:48:42 +0100 Subject: [PATCH 1/2] feat: enhance CodeExecutor with WebContainer support and terminal features --- README.md | 22 ++ angular.json | 8 +- package-lock.json | 7 + package.json | 1 + src/app/app.css | 223 ++++++++++++++- src/app/app.html | 170 +++++++---- src/app/app.ts | 394 +++++++++++++++++++++++++- src/app/terminal-clean-pipe.spec.ts | 8 + src/app/terminal-clean-pipe.ts | 15 + src/app/web-container.service.spec.ts | 16 ++ src/app/web-container.service.ts | 33 +++ 11 files changed, 822 insertions(+), 75 deletions(-) create mode 100644 src/app/terminal-clean-pipe.spec.ts create mode 100644 src/app/terminal-clean-pipe.ts create mode 100644 src/app/web-container.service.spec.ts create mode 100644 src/app/web-container.service.ts diff --git a/README.md b/README.md index 4531906..dfd36c6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,27 @@ # CodeExecutor +**CodeExecutor** is a web-based code editor and execution platform that allows users to write, edit, and run code directly in the browser. It supports multiple programming languages and provides an interactive development environment with real-time code execution capabilities powered by WebContainer/Docker technology. + +## Architecture Overview + +### Built with Angular 21 +The entire CodeExecutor platform is built with **Angular 21**, a modern TypeScript-based framework that delivers a responsive and intuitive user interface. The application provides a unified platform for code execution across different use cases with two distinct execution approaches. + +### Code Execution Methods + +#### Programming Languages (via Judge0) +For traditional programming languages (such as C, C++, Python, Java, etc.), CodeExecutor integrates with the **Judge0** API. Judge0 runs on an Ubuntu server and uses **isolate** to securely execute user code in sandboxed, lightweight containers. This ensures that code submissions are executed in isolated environments, preventing malicious or runaway code from affecting the host system or other submissions. + +#### Web Applications (Angular, React, Vue.js) +For web application execution, CodeExecutor provides two secure approaches: + +1. **Docker Approach**: Executes the uploaded Angular application code within an isolated Docker container. The compiled files are generated inside the container and returned to the client, ensuring complete isolation and security. + +2. **WebContainer Approach**: Leverages browser-based WebContainer technology to build and execute Angular applications entirely locally within the browser environment. This approach eliminates the need for a backend Express.js server to create Docker images and manage file compilation, providing a streamlined, client-side execution experience. + +### Future Frameworks +React and Vue.js support is coming soon, with both frameworks compatible with both Docker and WebContainer execution approaches. + This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.0.3. ## Development server diff --git a/angular.json b/angular.json index 9912e95..be7c782 100644 --- a/angular.json +++ b/angular.json @@ -67,7 +67,13 @@ "buildTarget": "code-executor:build:development" } }, - "defaultConfiguration": "development" + "defaultConfiguration": "development", + "options": { + "headers": { + "Cross-Origin-Embedder-Policy": "require-corp", + "Cross-Origin-Opener-Policy": "same-origin" + } + } }, "test": { "builder": "@angular/build:unit-test" diff --git a/package-lock.json b/package-lock.json index 3f67286..ccf0f9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.39.10", "@replit/codemirror-minimap": "^0.5.2", + "@webcontainer/api": "^1.6.1", "codemirror": "^6.0.2", "rxjs": "~7.8.0", "tslib": "^2.3.0" @@ -4300,6 +4301,12 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@webcontainer/api": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@webcontainer/api/-/api-1.6.1.tgz", + "integrity": "sha512-2RS2KiIw32BY1Icf6M1DvqSmcon9XICZCDgS29QJb2NmF12ZY2V5Ia+949hMKB3Wno+P/Y8W+sPP59PZeXSELg==", + "license": "MIT" + }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", diff --git a/package.json b/package.json index ceb803f..d596dfb 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.39.10", "@replit/codemirror-minimap": "^0.5.2", + "@webcontainer/api": "^1.6.1", "codemirror": "^6.0.2", "rxjs": "~7.8.0", "tslib": "^2.3.0" diff --git a/src/app/app.css b/src/app/app.css index 24d8496..7a8be4c 100644 --- a/src/app/app.css +++ b/src/app/app.css @@ -183,15 +183,15 @@ height: 100%; } -.output-content { +/* .output-content { flex: 1; overflow: auto; padding: 20px; -} +} */ -.preview-window { +/* .preview-window { height: 100%; -} +} */ .empty-state, .loading-state { @@ -209,16 +209,16 @@ opacity: 0.5; } -.output-text { - font-family: 'Courier New', Courier, monospace; /* Use a fixed-width font */ - white-space: pre-wrap; /* Preserve line breaks and spaces */ +/* .output-text { + font-family: 'Courier New', Courier, monospace; + white-space: pre-wrap; background: #1e1e1e; color: #f8f8f8; padding: 15px; border-radius: 4px; line-height: 1.4; overflow-x: auto; -} +} */ .spinner, .spinner-large { @@ -276,12 +276,12 @@ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } -.preview-iframe { +/* .preview-iframe { width: 100%; height: 100%; border: none; - background: white; /* Ensures you don't see the app's dark background if you have one */ -} + background: white; +}*/ .error-state { display: flex; @@ -312,3 +312,204 @@ white-space: pre-wrap; word-wrap: break-word; } */ + +/* .terminal-wrapper { + display: flex; + flex-direction: column; + flex: 1; + min-height: 200px; + background: #000; + border: 1px solid #333; +} */ + +.spinner-tiny { + width: 14px; + height: 14px; + stroke: #ffb86c; + stroke-width: 3; + fill: none; + animation: rotate 1s linear infinite; + margin-right: 8px; +} + +.terminal-header { + background: #252525; + padding: 8px 12px; + font-size: 12px; + color: #aaa; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid #333; +} + +/* .output-text { + margin: 0; + padding: 12px; + color: #00ff41; + font-family: 'Fira Code', 'Courier New', monospace; + font-size: 13px; + line-height: 1.5; + overflow-y: auto; + white-space: pre-wrap; + word-break: break-all; + background: #111; + height: 100%; +} */ + +.pulse-dot { + width: 8px; + height: 8px; + background: #ffb86c; + border-radius: 50%; + animation: pulse 1.5s infinite; +} + +.status-ready { + color: #50fa7b; + font-weight: bold; +} + +.copy-btn { + background: #333; + color: #eee; + border: none; + padding: 2px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 11px; +} + +.copy-btn:hover { + background: #444; +} + +@keyframes rotate { + 100% { + transform: rotate(360deg); + } +} + +@keyframes pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.3; + } + 100% { + opacity: 1; + } +} + +.loading-overlay { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 10; +} + +.output-content { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + position: relative; +} + +/* When the preview is active, give it most of the space */ +.preview-window { + flex: 1 1 auto; + display: flex; + flex-direction: column; + border-bottom: 2px solid #333; + min-height: 100px; + overflow: hidden; + width: 100%; +} + +.preview-iframe { + width: 100%; + height: 100%; + border: none; + background: white; /* If it stays white now, we know it's a code issue */ + display: block; +} + +.terminal-wrapper { + background: #000; + display: flex; + flex-direction: column; + /* If preview exists, terminal takes 30% height, otherwise 100% */ + height: 100%; + transition: height 0.3s ease; +} + +/* Adaptive height when preview is present */ +.has-preview .terminal-wrapper { + height: 30%; + min-height: 150px; +} + +.output-text { + flex: 1; + margin: 0; + padding: 10px; + overflow-y: auto; + font-family: 'Fira Code', monospace; + font-size: 12px; + color: #d4d4d4; + white-space: pre-wrap; +} + +.status-group { + display: flex; + align-items: center; +} + +.resize-bar { + height: 8px; + width: 100%; + background: linear-gradient(to bottom, #30363d 0%, #21262d 50%, #30363d 100%); + cursor: ns-resize; + border-top: 1px solid #30363d; + border-bottom: 1px solid #30363d; + transition: all 0.2s ease; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + user-select: none; + z-index: 10; +} + +.resize-bar:hover { + background: linear-gradient(to bottom, #58a6ff 0%, #1f6feb 50%, #58a6ff 100%); + border-color: #58a6ff; + height: 12px; +} + +.resize-bar.resizing { + background: #1f6feb; +} + +.resize-handle { + width: 40px; + height: 4px; + background: #58a6ff; + border-radius: 2px; + opacity: 0; + transition: opacity 0.2s ease; +} + +.resize-bar:hover .resize-handle { + opacity: 1; +} + +/* .minimized { + height: 30px !important; +} */ diff --git a/src/app/app.html b/src/app/app.html index 9ea6920..7684e07 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -176,82 +176,138 @@

CodeRunner

} -
- - @if (isPreviewMode() && previewHtmlRaw() && !previewError() && !isRunning()) { -
+
+ + @if (isPreviewMode()) { +
+ sandbox="allow-scripts allow-same-origin allow-forms allow-modals allow-popups" + style=" + width: 100%; + height: 100%; + border: none; + background: white; + pointer-events: none; + " + title="Angular Component Preview" + > +
- } @else if (isRunning()) { -
Compiling...
} - - @if (isPreviewMode() && previewError() && !previewHtmlRaw()) { -
- - - - - -

{{ previewError() }}

+ + @if (isPreviewMode() && consoleVisible()) { +
+
+
+ } + + + @if (output() && consoleVisible()) { +
+
+
+ @if (isRunning()) { + + + + {{ isContainerReady ? 'Syncing...' : 'Booting Engine...' }} + } @else { + ā— Console + } +
+ +
+ + + + +
+
+ +
{{ output() | terminalClean }}
} - - @if (!isPreviewMode() && !output() && !isRunning()) { + @if (!output() && !isRunning()) {
- - - - -

Run your code to see the output

+

Click "Run Code" to start the environment

} - - @if (isRunning()) { -
+ + @if (!consoleVisible && output()) { +
- } - - - @if (output() && !isRunning() && (!isPreviewMode() || !previewHtml())) { -
{{ formatErrorMessage(output()) }}
+ Show Console + }
diff --git a/src/app/app.ts b/src/app/app.ts index b367428..2a2cdf9 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -42,6 +42,8 @@ import { EditorState, Extension } from '@codemirror/state'; import { oneDark } from '@codemirror/theme-one-dark'; import { EditorView, keymap, lineNumbers } from '@codemirror/view'; import { showMinimap } from '@replit/codemirror-minimap'; +import { TerminalCleanPipe } from './terminal-clean-pipe'; +import { WebContainerService } from './web-container.service'; interface Language { id: number; @@ -66,17 +68,20 @@ interface ExecutionResult { @Component({ selector: 'app-root', - imports: [CommonModule, FormsModule], + imports: [CommonModule, FormsModule, TerminalCleanPipe], templateUrl: './app.html', styleUrl: './app.css', }) export class App implements OnInit, AfterViewInit, OnDestroy { @ViewChild('editorContainer') editorContainer!: ElementRef; @ViewChild('previewFrame') previewFrame!: ElementRef; + @ViewChild('terminalOutput') terminalRef!: ElementRef; private readonly http = inject(HttpClient); private readonly sanitizer = inject(DomSanitizer); + private readonly webcontainerService = inject(WebContainerService); private editorView: EditorView | null = null; private currentBlobUrl: string | null = null; + private iframeInitialized = false; code = signal(''); isRunning = signal(false); @@ -89,6 +94,7 @@ export class App implements OnInit, AfterViewInit, OnDestroy { previewHtmlRaw = signal(''); previewUrl = signal(null); output = signal('Ready...'); + previewHeight = signal(500); languages: Language[] = [ { @@ -290,17 +296,38 @@ export class AppComponent { }, ]; + isContainerReady = false; + constructor() { // This effect runs every time previewUrl is updated effect(() => { - const url = this.previewUrl(); - if (url && this.previewFrame) { - this.setupErrorTrap(); - } + // 1. We 'read' the signal to track changes + this.output(); + + // 2. We use requestAnimationFrame to wait for the browser to render the new HTML + requestAnimationFrame(() => { + if (this.terminalRef?.nativeElement) { + const el = this.terminalRef.nativeElement; + el.scrollTop = el.scrollHeight; + } + }); }); } ngOnInit() { + // Suppress WebContainer's frame_start.js error + window.addEventListener( + 'error', + (e) => { + if (e.filename?.includes('frame_start.js')) { + e.preventDefault(); + return false; + } + return true; + }, + true, + ); + const lang = this.languages.find((l) => l.id === this.selectedLanguageId()); this.currentLanguage.set(lang); this.code.set(lang?.template || ''); @@ -433,6 +460,8 @@ export class AppComponent { this.output.set(''); this.executionResult.set(null); this.previewHtml.set(null); + this.iframeInitialized = false; // Add this line + this.isContainerReady = false; this.initializeEditor(); } @@ -492,7 +521,7 @@ export class AppComponent { * Compiles an Angular component using the external compiler service * @param sourceCode The Angular component source code */ - compileAngularComponent(sourceCode: string) { + compileAngularComponent_old(sourceCode: string) { this.isRunning.set(true); this.isPreviewMode.set(true); this.output.set('Compiling Angular component... \nThis may take 1 minute.'); @@ -541,6 +570,278 @@ export class AppComponent { }); } + /** + * + * @param sourceCode + * @returns + */ + async compileAngularComponent(sourceCode: string) { + this.isRunning.set(true); + this.isPreviewMode.set(true); + this.output.set(''); // Clear logs for a fresh run + + try { + const webcontainer = await this.webcontainerService.init(); + + // ⚔ FAST UPDATE: If container is already running + if (this.isContainerReady) { + this.output.update((v) => v + '\n⚔ Updating source...'); + await webcontainer.fs.writeFile('/src/app.component.ts', sourceCode); + this.isRunning.set(false); + return; + } + + // šŸ—ļø FIRST TIME SETUP + this.output.set('šŸ—ļø Booting WebContainer...\n'); + + const files = { + 'package.json': { + file: { + contents: JSON.stringify({ + name: 'angular-live', + type: 'module', + dependencies: { + '@angular/animations': '17.3.0', + '@angular/common': '17.3.0', + '@angular/compiler': '17.3.0', + '@angular/core': '17.3.0', + '@angular/forms': '17.3.0', + '@angular/platform-browser': '17.3.0', + '@angular/platform-browser-dynamic': '17.3.0', + '@angular/router': '17.3.0', + '@angular-devkit/build-angular': '17.3.0', + '@angular/compiler-cli': '17.3.0', + '@analogjs/vite-plugin-angular': '1.3.0', + vite: '5.2.10', + typescript: '5.4.5', + rxjs: '7.8.1', + 'zone.js': '0.14.4', + tslib: '2.6.2', + tinyglobby: '0.2.0', + postcss: '8.4.38', + }, + scripts: { dev: 'vite --host' }, + }), + }, + }, + 'vite.config.ts': { + file: { + contents: ` +import { defineConfig } from 'vite'; +import angular from '@analogjs/vite-plugin-angular'; + +export default defineConfig({ + plugins: [angular()], + server: { + host: true, + port: 5173, + strictPort: true, + watch: { + usePolling: true, + }, + }, + resolve: { + mainFields: ['module', 'jsnext:main', 'jsnext'], + } +}); +`, + }, + }, + 'tsconfig.json': { + file: { + contents: JSON.stringify( + { + compilerOptions: { + target: 'ES2022', + useDefineForClassFields: false, + module: 'ESNext', + lib: ['ESNext', 'DOM'], + moduleResolution: 'Node', + experimentalDecorators: true, + emitDecoratorMetadata: true, + skipLibCheck: true, + baseUrl: './', + types: ['vite/client'], + }, + }, + null, + 2, + ), + }, + }, + src: { + directory: { + 'main.ts': { + file: { + contents: ` +import 'zone.js'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app.component'; + +console.log('šŸš€ main.ts loaded'); + +// Add error handling +bootstrapApplication(AppComponent) + .then(() => { + console.log('āœ… Angular app bootstrapped successfully'); + }) + .catch(err => { + console.error('āŒ Bootstrap error:', err); + const appRoot = document.querySelector('app-root'); + if (appRoot) { + appRoot.innerHTML = '
Bootstrap Error:\\n' + err.message + '\\n\\n' + (err.stack || '') + '
'; + } + }); +`, + }, + }, + 'app.component.ts': { + file: { + contents: sourceCode, + }, + }, + }, + }, + 'index.html': { + file: { + contents: ` + + + + + Angular Preview + + + +
Loading Angular...
+ + + + +`, + }, + }, + }; + + await webcontainer.mount(files); + + // šŸ“¦ INSTALL STEP + this.output.update((val) => val + 'šŸ“¦ npm install\n'); + const installProcess = await webcontainer.spawn('npm', ['install', '--legacy-peer-deps']); + + installProcess.output.pipeTo( + new WritableStream({ + write: (data) => { + if (data.includes('\x1b[1;1H') || data.includes('\x1b[2J')) { + this.output.set(''); + return; + } + + if (data.includes('\x1b[1G')) { + this.output.update((current) => { + const lastNewline = current.lastIndexOf('\n'); + const base = lastNewline > -1 ? current.substring(0, lastNewline + 1) : ''; + return base + data; + }); + } else { + this.output.update((v) => v + data); + } + }, + }), + ); + + const installCode = await installProcess.exit; + if (installCode !== 0) throw new Error('Installation failed'); + + // šŸš€ RUN STEP + this.output.update((val) => val + '\nšŸš€ Starting Dev Server...\n'); + const devProcess = await webcontainer.spawn('npm', ['run', 'dev']); + + devProcess.output.pipeTo( + new WritableStream({ + write: (data) => this.output.update((val) => val + data), + }), + ); + + // 🌐 SERVER READY - CRITICAL FIX HERE + webcontainer.on('server-ready', (port, url) => { + console.log('Server ready on port:', port); + console.log('Server URL:', url); + + // Prevent duplicate initialization + if (this.iframeInitialized) { + console.log('Iframe already initialized, skipping'); + return; + } + + this.output.update((v) => v + `\nāœ… Dev server ready at ${url}\n`); + + // Use Angular's zone to ensure change detection + setTimeout(() => { + const iframeEl = this.previewFrame?.nativeElement; + if (!iframeEl) { + console.error('Iframe element not found'); + return; + } + + // Clear any existing content and handlers + iframeEl.onload = null; + iframeEl.onerror = null; + + // Set up load handler BEFORE setting src + iframeEl.onload = () => { + console.log('āœ… Iframe loaded successfully'); + this.isContainerReady = true; + this.isRunning.set(false); + }; + + iframeEl.onerror = (e) => { + console.error('āŒ Iframe load error:', e); + this.output.update((v) => v + '\nāŒ Failed to load preview'); + this.isRunning.set(false); + }; + + console.log('Setting iframe src to:', url); + iframeEl.src = url; + this.iframeInitialized = true; + }, 1500); // Give Vite extra time to fully initialize + }); + } catch (error: any) { + this.output.update((val) => val + `\n\nāŒ Error: ${error.message}`); + this.isRunning.set(false); + } + } + // Clean up the string for the UI formatErrorMessage(rawLogs: string): string { if (!rawLogs) return ''; @@ -673,9 +974,90 @@ export class AppComponent { }); } + private cleanTerminalOutput(text: string): string { + if (!text) return ''; + + return ( + text + // 1. Kill the ANSI codes (The ones causing [32m etc.) + .replace(/\x1B\[[0-9;]*[A-Za-z]/g, '') + // 2. Kill the "Home" and "Clear" codes explicitly + .replace(/\x1B\[1;1H|\x1B\[0J|\x1B\[2J/g, '') + // 3. Remove the Backspace + Spinner junk + .replace(/.\x08/g, '') + // 4. Collapse the giant Vite empty gaps (3+ newlines into 1) + .replace(/\n{3,}/g, '\n\n') + // 5. Final safety: strip any stray Escape characters + .replace(/\x1B/g, '') + ); + } + + copyLogs() { + const plainText = this.cleanTerminalOutput(this.output()); + navigator.clipboard.writeText(plainText); + // Optional: show a "Copied!" toast + } + ngOnDestroy() { if (this.editorView) { this.editorView.destroy(); } } + + consoleVisible = signal(true); + consoleMinimized = signal(false); + consoleHeight = signal(30); // percentage + isResizing = false; + private startY = 0; + private startHeight = 0; + + toggleConsole() { + this.consoleVisible.set(!this.consoleVisible()); + } + + minimizeConsole() { + this.consoleMinimized.update((val) => !val); + } + + maximizeConsole() { + if (this.consoleHeight() < 90) { + this.consoleHeight.set(90); + } else { + this.consoleHeight.set(30); + } + } + + startResize(event: MouseEvent) { + event.preventDefault(); + this.isResizing = true; + this.startY = event.clientY; + this.startHeight = this.consoleHeight(); + + document.body.classList.add('resizing'); + + const mouseMoveHandler = (e: MouseEvent) => { + if (!this.isResizing) return; + + const containerHeight = (event.target as HTMLElement).parentElement?.offsetHeight || 600; + const deltaY = e.clientY - this.startY; + const deltaPercent = (deltaY / containerHeight) * 100; + + let newConsoleHeight = this.startHeight - deltaPercent; + + // Clamp between 10% and 90% + newConsoleHeight = Math.max(10, Math.min(90, newConsoleHeight)); + + this.consoleHeight.set(newConsoleHeight); + }; + + const mouseUpHandler = () => { + this.isResizing = false; + document.body.classList.remove('resizing'); + document.removeEventListener('mousemove', mouseMoveHandler); + document.removeEventListener('mouseup', mouseUpHandler); + }; + + document.addEventListener('mousemove', mouseMoveHandler); + document.addEventListener('mouseup', mouseUpHandler); + } } diff --git a/src/app/terminal-clean-pipe.spec.ts b/src/app/terminal-clean-pipe.spec.ts new file mode 100644 index 0000000..c09b4ef --- /dev/null +++ b/src/app/terminal-clean-pipe.spec.ts @@ -0,0 +1,8 @@ +import { TerminalCleanPipe } from './terminal-clean-pipe'; + +describe('TerminalCleanPipe', () => { + it('create an instance', () => { + const pipe = new TerminalCleanPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/app/terminal-clean-pipe.ts b/src/app/terminal-clean-pipe.ts new file mode 100644 index 0000000..69f0b10 --- /dev/null +++ b/src/app/terminal-clean-pipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'terminalClean', +}) +export class TerminalCleanPipe implements PipeTransform { + transform(value: string): string { + if (!value) return ''; + return value + .replaceAll(/\x1B\[[0-9;]*[A-Za-z]/g, '') // Remove ANSI + .replaceAll(/\x1B/g, '') // Remove stray Esc + .replaceAll(/.\x08/g, '') // Fix Spinner + .replaceAll(/\n{3,}/g, '\n\n'); // Collapse Vite gaps + } +} diff --git a/src/app/web-container.service.spec.ts b/src/app/web-container.service.spec.ts new file mode 100644 index 0000000..caafdc4 --- /dev/null +++ b/src/app/web-container.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { WebContainerService } from './web-container.service'; + +describe('WebContainerService', () => { + let service: WebContainerService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(WebContainerService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/web-container.service.ts b/src/app/web-container.service.ts new file mode 100644 index 0000000..832b3eb --- /dev/null +++ b/src/app/web-container.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { WebContainer } from '@webcontainer/api'; + +@Injectable({ + providedIn: 'root', +}) +export class WebContainerService { + private webcontainerInstance!: WebContainer; + + async init() { + if (!this.webcontainerInstance) { + // Boot the container + this.webcontainerInstance = await WebContainer.boot(); + } + return this.webcontainerInstance; + } + + async mountFiles(files: any) { + await this.webcontainerInstance.mount(files); + } + + async runCommand(command: string, args: string[]) { + const process = await this.webcontainerInstance.spawn(command, args); + process.output.pipeTo( + new WritableStream({ + write(data) { + console.log(data); + }, + }), + ); + return process.exit; + } +} From bd5d4a666a007c1bfc9e0e8644f1737905ea7711 Mon Sep 17 00:00:00 2001 From: ZAKARYA EL BAZY Date: Fri, 23 Jan 2026 10:48:55 +0100 Subject: [PATCH 2/2] refactor: optimize WebContainer initialization logic --- src/app/web-container.service.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/app/web-container.service.ts b/src/app/web-container.service.ts index 832b3eb..c8bbed7 100644 --- a/src/app/web-container.service.ts +++ b/src/app/web-container.service.ts @@ -5,14 +5,18 @@ import { WebContainer } from '@webcontainer/api'; providedIn: 'root', }) export class WebContainerService { + private instancePromise: Promise | null = null; private webcontainerInstance!: WebContainer; async init() { - if (!this.webcontainerInstance) { - // Boot the container - this.webcontainerInstance = await WebContainer.boot(); - } - return this.webcontainerInstance; + if (this.instancePromise) return this.instancePromise; + + this.instancePromise = WebContainer.boot().then((instance) => { + this.webcontainerInstance = instance; + return instance; + }); + + return this.instancePromise; } async mountFiles(files: any) {