diff --git a/apps/frontend/angular.json b/apps/frontend/angular.json
index 70a37c6..d41705f 100644
--- a/apps/frontend/angular.json
+++ b/apps/frontend/angular.json
@@ -19,6 +19,11 @@
{
"glob": "**/*",
"input": "public"
+ },
+ {
+ "glob": "**/*",
+ "input": "./node_modules/monaco-editor/min",
+ "output": "/assets/monaco-editor/min"
}
],
"styles": [
diff --git a/apps/frontend/bun.lock b/apps/frontend/bun.lock
index 413e4b1..0f22629 100644
--- a/apps/frontend/bun.lock
+++ b/apps/frontend/bun.lock
@@ -12,7 +12,9 @@
"@angular/platform-server": "^20.1.0",
"@angular/router": "^20.1.0",
"@angular/ssr": "^20.1.4",
+ "@materia-ui/ngx-monaco-editor": "^6.0.0",
"express": "^5.1.0",
+ "monaco-editor": "^0.52.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
},
@@ -284,6 +286,8 @@
"@lmdb/lmdb-win32-x64": ["@lmdb/lmdb-win32-x64@3.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-HqqKIhTbq6piJhkJpTTf3w1m/CgrmwXRAL9R9j7Ru5xdZSeO7Mg4AWiBC9B00uXR+LvVZKtUyRMVZfhmIZztmQ=="],
+ "@materia-ui/ngx-monaco-editor": ["@materia-ui/ngx-monaco-editor@6.0.0", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@angular/core": ">=13.0.0", "rxjs": ">=6.0.0" } }, "sha512-gTqNQjOGznZxOC0NlmKdKSGCJuTts8YmK4dsTQAGc5IgIV7cZdQWiW6AL742h0ruED6q0cAunEYjXT6jzHBoIQ=="],
+
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.13.3", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-bGwA78F/U5G2jrnsdRkPY3IwIwZeWUEfb5o764b79lb0rJmMT76TLwKhdNZOWakOQtedYefwIR4emisEMvInKA=="],
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="],
@@ -1086,6 +1090,8 @@
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": "bin/cmd.js" }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
+ "monaco-editor": ["monaco-editor@0.52.2", "", {}, "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ=="],
+
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index 64b34fb..d6e3649 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -32,7 +32,9 @@
"@angular/platform-server": "^20.1.0",
"@angular/router": "^20.1.0",
"@angular/ssr": "^20.1.4",
+ "@materia-ui/ngx-monaco-editor": "^6.0.0",
"express": "^5.1.0",
+ "monaco-editor": "^0.52.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0"
},
diff --git a/apps/frontend/src/app/app.css b/apps/frontend/src/app/app.css
index e69de29..8314a2c 100644
--- a/apps/frontend/src/app/app.css
+++ b/apps/frontend/src/app/app.css
@@ -0,0 +1,29 @@
+.editor-container {
+ margin-top: 2rem;
+ width: 100%;
+ max-width: 800px;
+}
+
+.editor-container h3 {
+ color: var(--gray-900);
+ margin-bottom: 1rem;
+ font-size: 1.5rem;
+ font-weight: 500;
+}
+
+.content {
+ flex-direction: column !important;
+ max-width: 1200px !important;
+ align-items: flex-start !important;
+}
+
+.left-side {
+ width: 100% !important;
+ max-width: none !important;
+}
+
+@media screen and (max-width: 650px) {
+ .editor-container {
+ width: 100%;
+ }
+}
\ No newline at end of file
diff --git a/apps/frontend/src/app/app.html b/apps/frontend/src/app/app.html
index 5ccc1c1..d532c2d 100644
--- a/apps/frontend/src/app/app.html
+++ b/apps/frontend/src/app/app.html
@@ -186,9 +186,22 @@

-
Hello, awesome developer!
-
Congratulations! Your app is running. 🎉
-
We are very excited that you are contributing to this great project. This is just the Angular template, so everything needs to be modified. [including this text]
+
Online Soroban Compiler
+
Write and test your Stellar smart contracts! 🚀
+
Use the Monaco Editor below to write Rust smart contracts for the Stellar blockchain using the Soroban SDK.
+
+
+
+
Rust Smart Contract Editor
+
+
+
diff --git a/apps/frontend/src/app/app.spec.ts b/apps/frontend/src/app/app.spec.ts
index 7b6fd55..41c26af 100644
--- a/apps/frontend/src/app/app.spec.ts
+++ b/apps/frontend/src/app/app.spec.ts
@@ -2,6 +2,20 @@ import { provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { App } from './app';
+// Mock Monaco Editor for tests
+(globalThis as unknown as { monaco: unknown }).monaco = {
+ editor: {
+ create: () => ({
+ getValue: () => '',
+ setValue: () => {},
+ dispose: () => {},
+ onDidChangeModelContent: () => {},
+ updateOptions: () => {},
+ layout: () => {}
+ })
+ }
+};
+
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
@@ -20,6 +34,6 @@ describe('App', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.querySelector('h1')?.textContent).toContain('Hello, frontend');
+ expect(compiled.querySelector('h1')?.textContent).toContain('Online Soroban Compiler');
});
});
diff --git a/apps/frontend/src/app/app.ts b/apps/frontend/src/app/app.ts
index ade0fcb..c19266b 100644
--- a/apps/frontend/src/app/app.ts
+++ b/apps/frontend/src/app/app.ts
@@ -1,12 +1,38 @@
import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
+import { FormsModule } from '@angular/forms';
+import { MonacoEditorComponent } from './monaco-editor/monaco-editor.component';
@Component({
selector: 'app-root',
- imports: [RouterOutlet],
+ imports: [RouterOutlet, MonacoEditorComponent, FormsModule],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
protected readonly title = signal('frontend');
+ protected readonly rustCode = signal(`// Sample Rust smart contract for Stellar
+use soroban_sdk::{contract, contractimpl, log, Env, Symbol, symbol_short};
+
+#[contract]
+pub struct HelloContract;
+
+#[contractimpl]
+impl HelloContract {
+ /// Says hello to someone
+ pub fn hello(env: Env, to: Symbol) -> Symbol {
+ log!(&env, "Hello {}", to);
+ symbol_short!("Hello")
+ }
+
+ /// Returns a greeting
+ pub fn greet(env: Env, name: Symbol) -> Symbol {
+ log!(&env, "Greeting {}", name);
+ symbol_short!("Greet")
+ }
+}`);
+
+ onEditorChange(value: string) {
+ this.rustCode.set(value);
+ }
}
diff --git a/apps/frontend/src/app/monaco-editor/monaco-editor.component.ts b/apps/frontend/src/app/monaco-editor/monaco-editor.component.ts
new file mode 100644
index 0000000..9ac7777
--- /dev/null
+++ b/apps/frontend/src/app/monaco-editor/monaco-editor.component.ts
@@ -0,0 +1,207 @@
+import { Component, Input, forwardRef, OnInit, OnDestroy, ViewEncapsulation, signal, effect, ViewChild, ElementRef, inject, PLATFORM_ID } from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
+import { isPlatformBrowser } from '@angular/common';
+
+interface MonacoEditor {
+ getValue(): string;
+ setValue(value: string): void;
+ dispose(): void;
+ onDidChangeModelContent(callback: () => void): void;
+ updateOptions(options: MonacoEditorOptions): void;
+ layout(): void;
+}
+
+interface MonacoEditorOptions {
+ value?: string;
+ language?: string;
+ theme?: string;
+ minimap?: { enabled: boolean };
+ automaticLayout?: boolean;
+ scrollBeyondLastLine?: boolean;
+ fontSize?: number;
+ wordWrap?: string;
+ lineNumbers?: string;
+ glyphMargin?: boolean;
+ folding?: boolean;
+ lineDecorationsWidth?: number;
+ lineNumbersMinChars?: number;
+ renderLineHighlight?: string;
+ contextmenu?: boolean;
+ mouseWheelZoom?: boolean;
+ readOnly?: boolean;
+}
+
+interface WindowRequire {
+ config(config: { paths: { vs: string } }): void;
+ (modules: string[], callback: () => void): void;
+}
+
+declare const monaco: {
+ editor: {
+ create(element: HTMLElement, options: MonacoEditorOptions): MonacoEditor;
+ };
+};
+
+declare global {
+ interface Window {
+ require: WindowRequire;
+ }
+}
+
+@Component({
+ selector: 'app-monaco-editor',
+ template: `
`,
+ styleUrls: [],
+ encapsulation: ViewEncapsulation.None,
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => MonacoEditorComponent),
+ multi: true
+ }
+ ],
+ imports: [FormsModule],
+ standalone: true
+})
+export class MonacoEditorComponent implements OnInit, OnDestroy, ControlValueAccessor {
+ @Input() language = 'rust';
+ @Input() theme = 'vs-dark';
+ @Input() height = '500px';
+ @Input() options: MonacoEditorOptions = {};
+
+ @ViewChild('editorContainer', { static: true }) editorContainer!: ElementRef
;
+
+ private editor: MonacoEditor | null = null;
+ private currentValue = signal('');
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ private onChange = (_value: string) => { /* no-op */ };
+ private onTouched = () => {};
+
+ private platformId = inject(PLATFORM_ID);
+
+ constructor() {
+ effect(() => {
+ if (this.editor && this.currentValue() !== this.editor.getValue()) {
+ this.editor.setValue(this.currentValue());
+ }
+ });
+ }
+
+ ngOnInit() {
+ this.loadMonacoEditor();
+ }
+
+ ngOnDestroy() {
+ if (this.editor) {
+ this.editor.dispose();
+ }
+ }
+
+ private loadMonacoEditor() {
+ // Only load Monaco Editor in the browser
+ if (!isPlatformBrowser(this.platformId)) {
+ return;
+ }
+
+ // Load Monaco Editor
+ if (typeof monaco === 'undefined') {
+ const script = document.createElement('script');
+ script.type = 'text/javascript';
+ script.src = '/assets/monaco-editor/min/vs/loader.js';
+ script.onload = () => {
+ window.require.config({
+ paths: { vs: '/assets/monaco-editor/min/vs' }
+ });
+ window.require(['vs/editor/editor.main'], () => {
+ this.initEditor();
+ });
+ };
+ script.onerror = () => {
+ this.loadFromCDN();
+ };
+ document.getElementsByTagName('head')[0].appendChild(script);
+ } else {
+ this.initEditor();
+ }
+ }
+
+ private loadFromCDN() {
+ if (!isPlatformBrowser(this.platformId)) {
+ return;
+ }
+
+ const script = document.createElement('script');
+ script.src = 'https://unpkg.com/monaco-editor@latest/min/vs/loader.js';
+ script.onload = () => {
+ window.require.config({
+ paths: { vs: 'https://unpkg.com/monaco-editor@latest/min/vs' }
+ });
+ window.require(['vs/editor/editor.main'], () => {
+ this.initEditor();
+ });
+ };
+ document.head.appendChild(script);
+ }
+
+ private initEditor() {
+ const editorElement = this.editorContainer.nativeElement;
+ if (!editorElement) {
+ setTimeout(() => this.initEditor(), 100);
+ return;
+ }
+
+ const defaultOptions: MonacoEditorOptions = {
+ value: this.currentValue(),
+ language: this.language,
+ theme: this.theme,
+ minimap: { enabled: false },
+ automaticLayout: true,
+ scrollBeyondLastLine: false,
+ fontSize: 14,
+ wordWrap: 'on',
+ lineNumbers: 'on',
+ glyphMargin: false,
+ folding: true,
+ lineDecorationsWidth: 20,
+ lineNumbersMinChars: 3,
+ renderLineHighlight: 'line',
+ contextmenu: true,
+ mouseWheelZoom: true,
+ ...this.options
+ };
+
+ this.editor = monaco.editor.create(editorElement, defaultOptions);
+
+ // Set up change listener
+ this.editor.onDidChangeModelContent(() => {
+ const value = this.editor?.getValue() || '';
+ this.currentValue.set(value);
+ this.onChange(value);
+ this.onTouched();
+ });
+
+ // Set height
+ editorElement.style.height = this.height;
+ this.editor.layout();
+ }
+
+ // ControlValueAccessor implementation
+ writeValue(value: string): void {
+ this.currentValue.set(value || '');
+ }
+
+ registerOnChange(fn: (value: string) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ if (this.editor) {
+ this.editor.updateOptions({ readOnly: isDisabled });
+ }
+ }
+}
\ No newline at end of file