UNPKG

csharp-wasm-runner

Version:

Compile and run C# code directly in the browser using Mono WebAssembly

372 lines (320 loc) 12.7 kB
/** * CSharp Browser Compiler Library * Compile and run C# code directly in the browser using Mono WebAssembly */ class CSharpBrowserCompiler { constructor(options = {}) { // Detect if we're running from node_modules const isNpmPackage = typeof window !== 'undefined' && (window.location?.pathname.includes('node_modules') || options.runtimePath?.includes('node_modules')); const defaultRuntimePath = isNpmPackage ? 'node_modules/csharp-browser-compiler/runtime/' : (options.runtimePath || './'); this.options = { monoConfig: options.monoConfig || `${defaultRuntimePath}mono-config.js`, runtimeJs: options.runtimeJs || 'runtime.js', dotnetJs: options.dotnetJs || `${defaultRuntimePath}dotnet.js`, dotnetWasm: options.dotnetWasm || `${defaultRuntimePath}dotnet.wasm`, assembliesPath: options.assembliesPath || `${defaultRuntimePath}managed/`, basePath: options.basePath || defaultRuntimePath, // Custom base path for asset loading useLibraryRuntime: options.useLibraryRuntime !== false, // Default to true onReady: options.onReady || (() => {}), onError: options.onError || ((error) => console.error(error)) }; this.isReady = false; this.readyPromise = null; this.customAssemblies = []; this.inputLines = []; this.outputLines = []; this.compileLog = []; // Initialize if auto-init is enabled (default) if (options.autoInit !== false) { this.init(); } } /** * Initialize the Mono runtime */ async init() { if (this.readyPromise) { return this.readyPromise; } this.readyPromise = new Promise(async (resolve, reject) => { try { // Set global config for mono window.config = window.config || {}; // If using library runtime, setup Module object first if (this.options.useLibraryRuntime) { window.Module = window.Module || { onRuntimeInitialized: function () { if (typeof MONO !== 'undefined' && MONO.mono_load_runtime_and_bcl) { MONO.mono_load_runtime_and_bcl( config.vfs_prefix, config.deploy_prefix, config.enable_debugging, config.file_list, function () { console.log('Mono runtime loaded'); } ); } } }; } // Load Mono configuration await this._loadScript(this.options.monoConfig); // Load runtime.js only if not using library runtime if (!this.options.useLibraryRuntime) { await this._loadScript(this.options.runtimeJs); } // Load dotnet.js which initializes the WASM runtime // Note: dotnet.js uses a deferred loading mechanism await this._loadScript(this.options.dotnetJs); // Wait for runtime to be ready await this._waitForRuntime(); // Additional wait to ensure full initialization await new Promise(resolve => setTimeout(resolve, 1000)); // Set custom base path if provided if (this.options.basePath) { BINDING.call_static_method( "[WasmRoslyn]WasmRoslyn.Program:SetBasePath", [this.options.basePath] ); } // Initialize the C# compiler service this._initializeCompilerService(); this.isReady = true; this.options.onReady(); resolve(); } catch (error) { this.options.onError(error); reject(error); } }); return this.readyPromise; } /** * Compile and run C# code * @param {string} code - C# source code * @param {Object} options - Execution options * @returns {Promise<CompileResult>} */ async run(code, options = {}) { await this.init(); // Set input lines if provided if (options.inputLines) { this.inputLines = options.inputLines; } return new Promise((resolve, reject) => { try { // Reset output this.outputLines = []; this.compileLog = []; // Create a temporary app object that mimics the original interface const app = { inputLines: this.inputLines, setRunLogArray: (lines) => { this.outputLines = lines; }, setCompileLog: (log) => { this.compileLog = log.split('\r\n'); } }; // Set up completion callback const originalSetRunLogArray = app.setRunLogArray; app.setRunLogArray = (lines) => { originalSetRunLogArray(lines); // Resolve with the result resolve({ success: true, output: lines, compileLog: this.compileLog }); }; // Handle compilation errors const originalSetCompileLog = app.setCompileLog; app.setCompileLog = (log) => { originalSetCompileLog(log); if (log.includes('Compilation error') || log.includes('Parse SyntaxTree Error')) { resolve({ success: false, output: [], compileLog: this.compileLog, error: 'Compilation failed' }); } }; // Call the C# Run method BINDING.call_static_method( "[WasmRoslyn]WasmRoslyn.Program:Run", [app, code, this.inputLines] ); } catch (error) { reject(error); } }); } /** * Compile C# code without running * @param {string} code - C# source code * @returns {Promise<CompileResult>} */ async compile(code) { await this.init(); return new Promise((resolve, reject) => { try { this.compileLog = []; const app = { setCompileLog: (log) => { this.compileLog = log.split('\r\n'); const success = log.includes('Compilation success'); resolve({ success: success, compileLog: this.compileLog }); } }; BINDING.call_static_method( "[WasmRoslyn]WasmRoslyn.Program:CompileOnly", [app, code] ); } catch (error) { reject(error); } }); } /** * Add custom assemblies (DLLs) to the compiler * @param {string[]} assemblies - Array of assembly URLs or names */ async addAssemblies(assemblies) { await this.init(); this.customAssemblies = [...this.customAssemblies, ...assemblies]; return new Promise((resolve, reject) => { try { const app = { assemblies: this.customAssemblies, onAssembliesLoaded: (success) => { if (success) { resolve(); } else { reject(new Error('Failed to load assemblies')); } } }; BINDING.call_static_method( "[WasmRoslyn]WasmRoslyn.Program:Process", [app] ); // Timeout after 30 seconds setTimeout(() => { reject(new Error('Assembly loading timeout')); }, 30000); } catch (error) { reject(error); } }); } /** * Get available assemblies * @returns {string[]} List of loaded assemblies */ getAssemblies() { return this.customAssemblies; } /** * Set input lines for Console.ReadLine() * @param {string[]} lines - Input lines */ setInputLines(lines) { this.inputLines = lines; } /** * Get output lines from last execution * @returns {string[]} Output lines */ getOutputLines() { return this.outputLines; } /** * Get compile log from last compilation * @returns {string[]} Compile log lines */ getCompileLog() { return this.compileLog; } // Private helper methods _loadScript(src) { return new Promise((resolve, reject) => { // Check if script already exists const existingScript = document.querySelector(`script[src="${src}"]`); if (existingScript) { resolve(); return; } const script = document.createElement('script'); script.src = src; script.type = 'text/javascript'; script.onload = () => { console.log(`Loaded: ${src}`); resolve(); }; script.onerror = (error) => { console.error(`Failed to load: ${src}`, error); reject(new Error(`Failed to load script: ${src}`)); }; document.head.appendChild(script); }); } _waitForRuntime() { return new Promise((resolve, reject) => { let attempts = 0; const maxAttempts = 100; // 10 seconds timeout const checkRuntime = () => { attempts++; if (typeof BINDING !== 'undefined' && typeof Module !== 'undefined' && typeof Module.mono_bind_static_method !== 'undefined') { console.log('Runtime ready'); resolve(); } else if (attempts >= maxAttempts) { reject(new Error('Timeout waiting for runtime initialization')); } else { setTimeout(checkRuntime, 100); } }; checkRuntime(); }); } _initializeCompilerService() { // Initialize the C# compiler service const app = { assemblies: [], displayAssemblies: (assemblies) => { console.log('Available assemblies:', assemblies); } }; // Create a dummy outputLog object const outputLog = { innerHTML: '' }; try { BINDING.call_static_method( "[WasmRoslyn]WasmRoslyn.Program:Main", [app, outputLog] ); } catch (error) { console.error('Failed to initialize compiler service:', error); throw error; } } } // Export for different module systems if (typeof module !== 'undefined' && module.exports) { module.exports = CSharpBrowserCompiler; } else if (typeof define === 'function' && define.amd) { define([], () => CSharpBrowserCompiler); } else { window.CSharpBrowserCompiler = CSharpBrowserCompiler; }