|
1 | 1 | /** |
2 | 2 | * T-Ruby WASM Loader for Playground |
3 | 3 | * |
4 | | - * Uses a sandboxed iframe to isolate WASM execution from browser extensions |
| 4 | + * Uses a sandboxed iframe with srcdoc to isolate WASM execution from browser extensions |
5 | 5 | * that might interfere with FinalizationRegistry and other APIs. |
6 | 6 | */ |
7 | 7 |
|
@@ -45,8 +45,188 @@ function generateRequestId(): string { |
45 | 45 | return `req_${++requestIdCounter}_${Date.now()}`; |
46 | 46 | } |
47 | 47 |
|
| 48 | +// Worker HTML content - embedded as string to use srcdoc |
| 49 | +// This creates an about:srcdoc origin iframe which most extensions don't target |
| 50 | +const WORKER_HTML = `<!DOCTYPE html> |
| 51 | +<html> |
| 52 | +<head> |
| 53 | + <meta charset="UTF-8"> |
| 54 | + <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data: https://cdn.jsdelivr.net;"> |
| 55 | + <title>T-Ruby WASM Worker</title> |
| 56 | +</head> |
| 57 | +<body> |
| 58 | +<script> |
| 59 | +// Protect native APIs before any extension can modify them |
| 60 | +(function() { |
| 61 | + const nativeFinalizationRegistry = window.FinalizationRegistry; |
| 62 | + const nativeWeakRef = window.WeakRef; |
| 63 | +
|
| 64 | + // Ensure these are the native implementations |
| 65 | + Object.defineProperty(window, 'FinalizationRegistry', { |
| 66 | + value: nativeFinalizationRegistry, |
| 67 | + writable: false, |
| 68 | + configurable: false |
| 69 | + }); |
| 70 | +
|
| 71 | + Object.defineProperty(window, 'WeakRef', { |
| 72 | + value: nativeWeakRef, |
| 73 | + writable: false, |
| 74 | + configurable: false |
| 75 | + }); |
| 76 | +})(); |
| 77 | +<\/script> |
| 78 | +<script type="module"> |
| 79 | +// CDN URLs |
| 80 | +const RUBY_WASM_CDN = 'https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.7.0/dist/browser/+esm'; |
| 81 | +const RUBY_WASM_BINARY = 'https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.7.0/dist/ruby+stdlib.wasm'; |
| 82 | +
|
| 83 | +// Bootstrap code for T-Ruby compiler |
| 84 | +const BOOTSTRAP_CODE = \` |
| 85 | +require "json" |
| 86 | +
|
| 87 | +$trb_compiler = nil |
| 88 | +
|
| 89 | +def get_compiler |
| 90 | + $trb_compiler ||= TRuby::Compiler.new |
| 91 | +end |
| 92 | +
|
| 93 | +def __trb_compile__(code) |
| 94 | + compiler = get_compiler |
| 95 | +
|
| 96 | + begin |
| 97 | + result = compiler.compile_string(code) |
| 98 | +
|
| 99 | + { |
| 100 | + success: result[:errors].empty?, |
| 101 | + ruby: result[:ruby] || "", |
| 102 | + rbs: result[:rbs] || "", |
| 103 | + errors: result[:errors] || [] |
| 104 | + }.to_json |
| 105 | + rescue TRuby::ParseError => e |
| 106 | + { |
| 107 | + success: false, |
| 108 | + ruby: "", |
| 109 | + rbs: "", |
| 110 | + errors: [e.message] |
| 111 | + }.to_json |
| 112 | + rescue StandardError => e |
| 113 | + { |
| 114 | + success: false, |
| 115 | + ruby: "", |
| 116 | + rbs: "", |
| 117 | + errors: ["Compilation error: " + e.message] |
| 118 | + }.to_json |
| 119 | + end |
| 120 | +end |
| 121 | +
|
| 122 | +def __trb_health_check__ |
| 123 | + { |
| 124 | + loaded: defined?(TRuby) == "constant", |
| 125 | + version: defined?(TRuby::VERSION) ? TRuby::VERSION : "unknown", |
| 126 | + ruby_version: RUBY_VERSION |
| 127 | + }.to_json |
| 128 | +end |
| 129 | +\`; |
| 130 | +
|
| 131 | +let vm = null; |
| 132 | +let isReady = false; |
| 133 | +
|
| 134 | +// Send message to parent |
| 135 | +function sendToParent(type, data, requestId) { |
| 136 | + parent.postMessage({ type, data, requestId }, '*'); |
| 137 | +} |
| 138 | +
|
| 139 | +// Initialize Ruby VM |
| 140 | +async function initialize() { |
| 141 | + try { |
| 142 | + console.log('[WASM Worker] Step 1: Loading Ruby WASM module...'); |
| 143 | + sendToParent('progress', { message: 'Loading Ruby runtime...', progress: 10 }); |
| 144 | +
|
| 145 | + const { DefaultRubyVM } = await import(RUBY_WASM_CDN); |
| 146 | + console.log('[WASM Worker] Step 1 complete'); |
| 147 | +
|
| 148 | + console.log('[WASM Worker] Step 2: Fetching WASM binary...'); |
| 149 | + sendToParent('progress', { message: 'Downloading Ruby WASM binary...', progress: 30 }); |
| 150 | +
|
| 151 | + const response = await fetch(RUBY_WASM_BINARY); |
| 152 | + const wasmModule = await WebAssembly.compileStreaming(response); |
| 153 | + console.log('[WASM Worker] Step 2 complete'); |
| 154 | +
|
| 155 | + console.log('[WASM Worker] Step 3: Initializing Ruby VM...'); |
| 156 | + sendToParent('progress', { message: 'Initializing Ruby VM...', progress: 60 }); |
| 157 | +
|
| 158 | + const result = await DefaultRubyVM(wasmModule); |
| 159 | + vm = result.vm; |
| 160 | + console.log('[WASM Worker] Step 3 complete'); |
| 161 | +
|
| 162 | + console.log('[WASM Worker] Step 4: Loading T-Ruby bootstrap...'); |
| 163 | + sendToParent('progress', { message: 'Loading T-Ruby compiler...', progress: 80 }); |
| 164 | +
|
| 165 | + vm.eval(BOOTSTRAP_CODE); |
| 166 | + console.log('[WASM Worker] Step 4 complete'); |
| 167 | +
|
| 168 | + // Health check |
| 169 | + const healthResult = vm.eval('__trb_health_check__'); |
| 170 | + console.log('[WASM Worker] Health check:', healthResult.toString()); |
| 171 | +
|
| 172 | + isReady = true; |
| 173 | + sendToParent('ready', { health: JSON.parse(healthResult.toString()) }); |
| 174 | +
|
| 175 | + } catch (error) { |
| 176 | + console.error('[WASM Worker] Init error:', error); |
| 177 | + sendToParent('error', { message: error.message }); |
| 178 | + } |
| 179 | +} |
| 180 | +
|
| 181 | +// Compile code |
| 182 | +function compile(code, requestId) { |
| 183 | + if (!isReady || !vm) { |
| 184 | + sendToParent('compile-result', { |
| 185 | + success: false, |
| 186 | + errors: ['Compiler not ready'] |
| 187 | + }, requestId); |
| 188 | + return; |
| 189 | + } |
| 190 | +
|
| 191 | + try { |
| 192 | + console.log('[WASM Worker] Compiling:', code.substring(0, 50) + '...'); |
| 193 | + const resultJson = vm.eval('__trb_compile__(' + JSON.stringify(code) + ')'); |
| 194 | + const result = JSON.parse(resultJson.toString()); |
| 195 | + console.log('[WASM Worker] Compile result:', result); |
| 196 | + sendToParent('compile-result', result, requestId); |
| 197 | + } catch (error) { |
| 198 | + console.error('[WASM Worker] Compile error:', error); |
| 199 | + sendToParent('compile-result', { |
| 200 | + success: false, |
| 201 | + ruby: '', |
| 202 | + rbs: '', |
| 203 | + errors: [error.message] |
| 204 | + }, requestId); |
| 205 | + } |
| 206 | +} |
| 207 | +
|
| 208 | +// Listen for messages from parent |
| 209 | +window.addEventListener('message', (event) => { |
| 210 | + const { type, data, requestId } = event.data || {}; |
| 211 | +
|
| 212 | + switch (type) { |
| 213 | + case 'init': |
| 214 | + initialize(); |
| 215 | + break; |
| 216 | + case 'compile': |
| 217 | + compile(data.code, requestId); |
| 218 | + break; |
| 219 | + } |
| 220 | +}); |
| 221 | +
|
| 222 | +// Signal that worker is loaded |
| 223 | +sendToParent('loaded', {}); |
| 224 | +<\/script> |
| 225 | +</body> |
| 226 | +</html>`; |
| 227 | + |
48 | 228 | /** |
49 | | - * Load the T-Ruby WASM compiler using sandboxed iframe |
| 229 | + * Load the T-Ruby WASM compiler using sandboxed iframe with srcdoc |
50 | 230 | * Returns cached instance if already loaded |
51 | 231 | */ |
52 | 232 | export async function loadTRubyCompiler( |
@@ -79,18 +259,21 @@ async function doLoadCompiler( |
79 | 259 | onProgress?: (progress: LoadingProgress) => void |
80 | 260 | ): Promise<TRubyCompiler> { |
81 | 261 | return new Promise((resolve, reject) => { |
82 | | - console.log('[T-Ruby] Creating sandboxed iframe for WASM execution...'); |
| 262 | + console.log('[T-Ruby] Creating sandboxed srcdoc iframe for WASM execution...'); |
83 | 263 | onProgress?.({ |
84 | 264 | state: 'loading', |
85 | 265 | message: 'Initializing compiler sandbox...', |
86 | 266 | progress: 5 |
87 | 267 | }); |
88 | 268 |
|
89 | | - // Create sandboxed iframe |
| 269 | + // Create sandboxed iframe with srcdoc |
| 270 | + // Using srcdoc creates an about:srcdoc origin which most extensions don't target |
90 | 271 | const iframe = document.createElement('iframe'); |
91 | 272 | iframe.style.display = 'none'; |
| 273 | + // allow-scripts: needed to run JavaScript |
| 274 | + // allow-same-origin: needed for postMessage to work properly |
92 | 275 | iframe.sandbox.add('allow-scripts'); |
93 | | - iframe.src = '/wasm-worker.html'; |
| 276 | + iframe.srcdoc = WORKER_HTML; |
94 | 277 |
|
95 | 278 | // Message handler |
96 | 279 | const messageHandler = (event: MessageEvent) => { |
|
0 commit comments