This commit completes the self-hosting of Ritz's debug console and
introduces the @external decorator for elegant JavaScript bridge functions.
## Major Features
### 1. Self-Hosted Debug Console (DebugConsole.py)
- Entire debug console UI written in Ritz itself
- Signal-based state management for toggle functionality
- Displays runtime stats (memory, signals, DOM nodes, logs)
- Live reactive updates when signals change in main app
- Keyboard shortcut support (Ctrl+`)
### 2. @external Decorator for JavaScript Bridges
- Pythonic decorator syntax: @external or @bridge
- Parser automatically recognizes and tracks external functions
- Codegen emits proper LLVM declarations
- No more hacky string checks (startswith("js_"))
- Clean, declarative bridge function definitions
Example:
```python
@external
def js_toggle_debug_console():
pass
def toggle():
js_toggle_debug_console() # Clean call to JS bridge
```
### 3. Enhanced RitzLoader Debug Mode
- `new RitzLoader({ debug: true })` auto-loads DebugConsole.wasm
- Keyboard shortcut registration (Ctrl+` toggle)
- Runtime stats injection (memory, signals, DOM nodes)
- Automatic status element hiding when debug enabled
- Debug console isolated in separate WASM runtime
## Bug Fixes
- Fixed reactive text node scoping (prevent global name collisions)
- Fixed CSS specificity for debug console status (use #ritz-debug)
- Fixed signal cross-contamination between runtimes (scope to rootElement)
- Fixed parser to handle expression statements in function bodies
- Fixed conditional branch rendering (append children to wrapper divs)
## Infrastructure Improvements
- Added H4 and Pre elements to syntax.py
- Parser handles @external decorator on top-level functions
- Codegen pre-collects external functions before emitting declarations
- Runtime scopes querySelector to prevent cross-runtime interference
- Debug stats update reactively on every signal change
## Files Added/Modified
New files:
- examples/runtime/DebugConsole.py - Self-hosted debug UI
- examples/runtime/DebugConsole.wasm - Compiled debug console (1.8KB)
- PHASE4_CLEANUP.md - Next phase planning document
Modified:
- ritz/parser.py - @external decorator support
- ritz/llvm_codegen_signals.py - External function handling
- ritz/syntax.py - Added external/bridge decorators, H4, Pre
- examples/runtime/ritz-runtime.js - Debug mode, stats updates
- examples/runtime/styles.css - Merged debug styles
- examples/template.html - Debug mode enabled by default
- examples/highfive/HighFive.html - Simplified using template
## Known Issues (Addressed in PHASE4_CLEANUP.md)
1. Debug console shifts page RIGHT (should shift LEFT)
2. Toggle implementation un-Ritzy (calls JS to manipulate DOM)
- Should use reactive class binding + @event decorator
- Needs event object bridge for keyboard events
3. Missing test infrastructure
## Next Steps (Phase 4)
See PHASE4_CLEANUP.md for detailed plan:
- Implement reactive class binding (signal-driven classes)
- Add @event decorator for declarative event registration
- Build event object bridge (access event.key, event.ctrlKey)
- Create Python test mode (execute without browser)
- Add comprehensive test suite
- Update all examples to use new patterns
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
545 lines
20 KiB
JavaScript
545 lines
20 KiB
JavaScript
/**
|
|
* Ritz Runtime - WebAssembly DOM Bridge
|
|
*
|
|
* Provides the runtime environment for Ritz components compiled to WASM.
|
|
*/
|
|
|
|
class RitzRuntime {
|
|
constructor(rootElement) {
|
|
this.rootElement = rootElement || document.getElementById('app');
|
|
this.nodes = new Map();
|
|
this.nextNodeId = 1;
|
|
this.log = [];
|
|
this.wasmInstance = null;
|
|
this.debugEnabled = true;
|
|
}
|
|
|
|
// ===== DOM Manipulation Bridge =====
|
|
|
|
js_create_element(tagPtr) {
|
|
const tag = this.readString(tagPtr);
|
|
const element = document.createElement(tag);
|
|
const id = this.nextNodeId++;
|
|
this.nodes.set(id, element);
|
|
this._log(`createElement("${tag}") = ${id}`);
|
|
return id;
|
|
}
|
|
|
|
js_create_text(textPtr) {
|
|
const text = this.readString(textPtr);
|
|
const textNode = document.createTextNode(text);
|
|
const id = this.nextNodeId++;
|
|
this.nodes.set(id, textNode);
|
|
this._log(`createTextNode("${text}") = ${id}`);
|
|
return id;
|
|
}
|
|
|
|
js_append_child(parentId, childId) {
|
|
const parent = this.nodes.get(parentId);
|
|
const child = this.nodes.get(childId);
|
|
if (parent && child) {
|
|
parent.appendChild(child);
|
|
this._log(`appendChild(${parentId}, ${childId})`);
|
|
}
|
|
}
|
|
|
|
js_set_attribute(elemId, namePtr, valuePtr) {
|
|
const element = this.nodes.get(elemId);
|
|
const name = this.readString(namePtr);
|
|
const value = this.readString(valuePtr);
|
|
if (element) {
|
|
if (name === 'class') {
|
|
element.className = value;
|
|
} else {
|
|
element.setAttribute(name, value);
|
|
}
|
|
this._log(`setAttribute(${elemId}, "${name}", "${value}")`);
|
|
}
|
|
}
|
|
|
|
js_bind_event(elemId, eventNamePtr, handlerNamePtr) {
|
|
const element = this.nodes.get(elemId);
|
|
const eventName = this.readString(eventNamePtr);
|
|
const handlerName = this.readString(handlerNamePtr);
|
|
|
|
if (element && this.wasmInstance) {
|
|
const handler = this.wasmInstance.exports[handlerName];
|
|
if (handler) {
|
|
element.addEventListener(eventName, () => {
|
|
this._log(`[Event] ${eventName} → ${handlerName}()`);
|
|
handler();
|
|
this.updateSignals();
|
|
});
|
|
this._log(`bindEvent(${elemId}, "${eventName}", "${handlerName}")`);
|
|
} else {
|
|
console.error(`Handler ${handlerName} not found in WASM exports`);
|
|
}
|
|
}
|
|
}
|
|
|
|
js_toggle_debug_console() {
|
|
// Bridge function to toggle debug console from WASM
|
|
if (window.ritzLoader) {
|
|
window.ritzLoader.toggleDebugConsole();
|
|
}
|
|
this._log('[Debug] Toggle debug console from WASM');
|
|
}
|
|
|
|
js_set_root(elemId) {
|
|
const element = this.nodes.get(elemId);
|
|
if (element) {
|
|
this.rootElement.innerHTML = '';
|
|
this.rootElement.appendChild(element);
|
|
this._log(`setRoot(${elemId})`);
|
|
}
|
|
}
|
|
|
|
js_update_text_content(nodeId, textPtr) {
|
|
const node = this.nodes.get(nodeId);
|
|
const text = this.readString(textPtr);
|
|
if (node) {
|
|
node.textContent = text;
|
|
this._log(`updateTextContent(${nodeId}, "${text}")`);
|
|
}
|
|
}
|
|
|
|
js_create_text_with_int(prefixPtr, intValue) {
|
|
const prefix = this.readString(prefixPtr);
|
|
const text = prefix + intValue.toString();
|
|
const textNode = document.createTextNode(text);
|
|
const id = this.nextNodeId++;
|
|
this.nodes.set(id, textNode);
|
|
this._log(`createTextWithInt("${prefix}", ${intValue}) = "${text}" (id=${id})`);
|
|
return id;
|
|
}
|
|
|
|
js_update_text_with_int(nodeId, prefixPtr, intValue) {
|
|
const node = this.nodes.get(nodeId);
|
|
const prefix = this.readString(prefixPtr);
|
|
const text = prefix + intValue.toString();
|
|
if (node) {
|
|
node.textContent = text;
|
|
this._log(`updateTextWithInt(${nodeId}, "${prefix}", ${intValue}) = "${text}"`);
|
|
}
|
|
}
|
|
|
|
// ===== Signal Management =====
|
|
|
|
updateSignals() {
|
|
// Discover and update all signals
|
|
if (!this.wasmInstance) return;
|
|
|
|
const exports = this.wasmInstance.exports;
|
|
|
|
// Find all get_* functions (signal getters)
|
|
for (const [name, fn] of Object.entries(exports)) {
|
|
if (name.startsWith('get_') && typeof fn === 'function') {
|
|
const signalName = name.substring(4); // Remove 'get_' prefix
|
|
const value = fn();
|
|
this._log(`[Signal] ${signalName} = ${value}`);
|
|
|
|
// Update DOM elements bound to this signal
|
|
// TODO: Track signal-to-DOM bindings during render for efficiency
|
|
this.updateSignalDisplay(signalName, value);
|
|
}
|
|
}
|
|
|
|
// Re-evaluate all reactive conditionals
|
|
for (const [name, fn] of Object.entries(exports)) {
|
|
if (name.startsWith('reevaluate_conditional_') && typeof fn === 'function') {
|
|
fn();
|
|
this._log(`[Reactive] ${name}()`);
|
|
}
|
|
}
|
|
|
|
// Re-evaluate all reactive match statements
|
|
for (const [name, fn] of Object.entries(exports)) {
|
|
if (name.startsWith('reevaluate_match_') && typeof fn === 'function') {
|
|
fn();
|
|
this._log(`[Reactive] ${name}()`);
|
|
}
|
|
}
|
|
|
|
// Update debug console stats if debug mode is enabled
|
|
if (window.ritzLoader && this.debugEnabled) {
|
|
window.ritzLoader.updateDebugStats();
|
|
}
|
|
}
|
|
|
|
updateSignalDisplay(signalName, value) {
|
|
// Find DOM elements that should display this signal
|
|
// IMPORTANT: Only search within this runtime's root element to avoid cross-contamination
|
|
const selectors = [
|
|
`[data-signal="${signalName}"]`,
|
|
`.${signalName}`,
|
|
'.count', // Hardcoded for demo - TODO: track bindings properly
|
|
];
|
|
|
|
selectors.forEach(selector => {
|
|
// Only query within this runtime's root element, not the entire document
|
|
const elements = this.rootElement.querySelectorAll(selector);
|
|
elements.forEach(el => {
|
|
// Check if text contains the signal name or looks like a counter
|
|
const text = el.textContent;
|
|
if (text.includes(signalName) || text.match(/\d+/)) {
|
|
// Replace the number in the text
|
|
el.textContent = text.replace(/\d+/, value);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// ===== Reactive Conditional Rendering =====
|
|
|
|
js_create_marker() {
|
|
// Create a comment node as a marker for conditional insertion
|
|
const marker = document.createComment('conditional-marker');
|
|
const id = this.nextNodeId++;
|
|
this.nodes.set(id, marker);
|
|
this._log(`Created marker node ${id}`);
|
|
return id;
|
|
}
|
|
|
|
js_replace_at_marker(markerId, oldNodeId, newNodeId) {
|
|
// Replace DOM node at marker position
|
|
const marker = this.nodes.get(markerId);
|
|
if (!marker) {
|
|
console.error(`[replace_at_marker] Marker ${markerId} not found`);
|
|
return;
|
|
}
|
|
|
|
const parent = marker.parentNode;
|
|
if (!parent) {
|
|
console.error(`[replace_at_marker] Marker ${markerId} has no parent`);
|
|
return;
|
|
}
|
|
|
|
this._log(`[replace_at_marker] marker=${markerId}, old=${oldNodeId}, new=${newNodeId}`);
|
|
|
|
// Remove old node if it exists (0 means no old node to remove)
|
|
if (oldNodeId !== 0) {
|
|
const oldNode = this.nodes.get(oldNodeId);
|
|
if (!oldNode) {
|
|
console.warn(`[replace_at_marker] Old node ${oldNodeId} not found in registry`);
|
|
} else if (!oldNode.parentNode) {
|
|
console.warn(`[replace_at_marker] Old node ${oldNodeId} not in DOM`);
|
|
} else if (oldNode.parentNode !== parent) {
|
|
console.warn(`[replace_at_marker] Old node ${oldNodeId} in wrong parent`);
|
|
oldNode.parentNode.removeChild(oldNode);
|
|
this._log(`[replace_at_marker] Removed ${oldNodeId} from wrong parent`);
|
|
} else {
|
|
// Remove from correct parent
|
|
parent.removeChild(oldNode);
|
|
this._log(`[replace_at_marker] Removed ${oldNodeId} from correct parent`);
|
|
}
|
|
}
|
|
|
|
// Insert new node after marker (only if newNodeId is not 0)
|
|
if (newNodeId !== 0) {
|
|
const newNode = this.nodes.get(newNodeId);
|
|
if (!newNode) {
|
|
console.error(`[replace_at_marker] New node ${newNodeId} not found`);
|
|
return;
|
|
}
|
|
|
|
// Insert right after the marker
|
|
// If newNode is already in DOM elsewhere, this will move it
|
|
const nextSibling = marker.nextSibling;
|
|
if (nextSibling) {
|
|
parent.insertBefore(newNode, nextSibling);
|
|
this._log(`[replace_at_marker] Inserted ${newNodeId} before sibling`);
|
|
} else {
|
|
parent.appendChild(newNode);
|
|
this._log(`[replace_at_marker] Appended ${newNodeId} (no sibling)`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===== Memory & String Utilities =====
|
|
|
|
readString(ptr) {
|
|
if (!this.memory) return '';
|
|
const bytes = new Uint8Array(this.memory.buffer, ptr);
|
|
let length = 0;
|
|
while (bytes[length] !== 0) length++;
|
|
return new TextDecoder().decode(bytes.slice(0, length));
|
|
}
|
|
|
|
setMemory(memory) {
|
|
this.memory = memory;
|
|
}
|
|
|
|
setWasmInstance(instance) {
|
|
this.wasmInstance = instance;
|
|
}
|
|
|
|
// ===== Logging & Debug =====
|
|
|
|
_log(message) {
|
|
if (this.debugEnabled) {
|
|
this.log.push(message);
|
|
}
|
|
}
|
|
|
|
getLog() {
|
|
return this.log;
|
|
}
|
|
|
|
clearLog() {
|
|
this.log = [];
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* RitzLoader - Simplified WASM loading
|
|
*/
|
|
class RitzLoader {
|
|
constructor(options = {}) {
|
|
this.rootElement = options.rootElement || document.getElementById('app');
|
|
this.statusElement = options.statusElement || document.getElementById('status');
|
|
this.debug = options.debug !== undefined ? options.debug : false;
|
|
this.onLoad = options.onLoad || (() => {});
|
|
this.onError = options.onError || ((err) => console.error(err));
|
|
this.debugConsoleRuntime = null;
|
|
}
|
|
|
|
async load(wasmPath, componentName = null, props = {}) {
|
|
try {
|
|
// Auto-detect component name from wasm filename if not provided
|
|
if (!componentName) {
|
|
// Extract from path: "Main.wasm" → "Main", "path/to/Main.wasm" → "Main"
|
|
componentName = wasmPath.split('/').pop().replace('.wasm', '');
|
|
}
|
|
|
|
this.setStatus('⏳ Creating runtime...');
|
|
const runtime = new RitzRuntime(this.rootElement);
|
|
|
|
this.setStatus(`⏳ Loading ${wasmPath}...`);
|
|
const response = await fetch(wasmPath);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to load ${wasmPath}: ${response.status}`);
|
|
}
|
|
|
|
const wasmBytes = await response.arrayBuffer();
|
|
|
|
this.setStatus('⏳ Instantiating WASM...');
|
|
|
|
// Create imports for WASM
|
|
const imports = {
|
|
env: {
|
|
js_create_element: (ptr) => runtime.js_create_element(ptr),
|
|
js_create_text: (ptr) => runtime.js_create_text(ptr),
|
|
js_create_text_with_int: (p, i) => runtime.js_create_text_with_int(p, i),
|
|
js_append_child: (p, c) => runtime.js_append_child(p, c),
|
|
js_set_attribute: (e, n, v) => runtime.js_set_attribute(e, n, v),
|
|
js_set_root: (e) => runtime.js_set_root(e),
|
|
js_bind_event: (e, n, h) => runtime.js_bind_event(e, n, h),
|
|
js_update_text_content: (n, t) => runtime.js_update_text_content(n, t),
|
|
js_update_text_with_int: (n, p, i) => runtime.js_update_text_with_int(n, p, i),
|
|
js_create_marker: () => runtime.js_create_marker(),
|
|
js_replace_at_marker: (m, o, n) => runtime.js_replace_at_marker(m, o, n),
|
|
}
|
|
};
|
|
|
|
const { instance } = await WebAssembly.instantiate(wasmBytes, imports);
|
|
|
|
// Setup runtime
|
|
runtime.setMemory(instance.exports.memory);
|
|
runtime.setWasmInstance(instance);
|
|
|
|
this.setStatus(`⏳ Rendering ${componentName}...`);
|
|
|
|
// Call the main entry point (which renders top-level component and sets root)
|
|
const mainFn = instance.exports.main;
|
|
if (!mainFn) {
|
|
throw new Error(`main function not found in WASM exports`);
|
|
}
|
|
|
|
mainFn();
|
|
|
|
// Update signals
|
|
runtime.updateSignals();
|
|
|
|
this.setStatus(`✅ ${componentName} loaded!`, 'success');
|
|
|
|
// Make runtime globally available for debugging
|
|
window.ritzRuntime = runtime;
|
|
window.ritzInstance = instance;
|
|
|
|
this.onLoad(runtime, instance);
|
|
|
|
// Load debug console if debug mode is enabled
|
|
if (this.debug) {
|
|
await this.loadDebugConsole(componentName);
|
|
}
|
|
|
|
return { runtime, instance };
|
|
|
|
} catch (error) {
|
|
this.setStatus(`❌ Error: ${error.message}`, 'error');
|
|
this.onError(error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async loadDebugConsole(appComponentName) {
|
|
try {
|
|
console.log('[Debug] Loading debug console...');
|
|
|
|
// Create a container for the debug console
|
|
const debugContainer = document.createElement('div');
|
|
debugContainer.id = 'debug-console-root';
|
|
document.body.appendChild(debugContainer);
|
|
|
|
console.log('[Debug] Created container:', debugContainer);
|
|
|
|
// Create debug console runtime
|
|
const debugRuntime = new RitzRuntime(debugContainer);
|
|
debugRuntime.debugEnabled = false; // Don't log debug console's own operations
|
|
|
|
// Load DebugConsole.wasm from runtime directory
|
|
const debugWasmPath = '../runtime/DebugConsole.wasm';
|
|
console.log('[Debug] Fetching:', debugWasmPath);
|
|
|
|
const response = await fetch(debugWasmPath);
|
|
if (!response.ok) {
|
|
console.warn('Could not load debug console:', response.status);
|
|
return;
|
|
}
|
|
|
|
console.log('[Debug] DebugConsole.wasm loaded successfully');
|
|
|
|
const wasmBytes = await response.arrayBuffer();
|
|
|
|
// Create imports for debug console WASM
|
|
const imports = {
|
|
env: {
|
|
js_create_element: (ptr) => debugRuntime.js_create_element(ptr),
|
|
js_create_text: (ptr) => debugRuntime.js_create_text(ptr),
|
|
js_create_text_with_int: (p, i) => debugRuntime.js_create_text_with_int(p, i),
|
|
js_append_child: (p, c) => debugRuntime.js_append_child(p, c),
|
|
js_set_attribute: (e, n, v) => debugRuntime.js_set_attribute(e, n, v),
|
|
js_set_root: (e) => debugRuntime.js_set_root(e),
|
|
js_bind_event: (e, n, h) => debugRuntime.js_bind_event(e, n, h),
|
|
js_update_text_content: (n, t) => debugRuntime.js_update_text_content(n, t),
|
|
js_update_text_with_int: (n, p, i) => debugRuntime.js_update_text_with_int(n, p, i),
|
|
js_create_marker: () => debugRuntime.js_create_marker(),
|
|
js_replace_at_marker: (m, o, n) => debugRuntime.js_replace_at_marker(m, o, n),
|
|
js_toggle_debug_console: () => debugRuntime.js_toggle_debug_console(),
|
|
}
|
|
};
|
|
|
|
const { instance } = await WebAssembly.instantiate(wasmBytes, imports);
|
|
|
|
// Setup debug runtime
|
|
debugRuntime.setMemory(instance.exports.memory);
|
|
debugRuntime.setWasmInstance(instance);
|
|
|
|
// Render debug console
|
|
console.log('[Debug] Rendering debug console...');
|
|
instance.exports.main();
|
|
debugRuntime.updateSignals();
|
|
|
|
this.debugConsoleRuntime = debugRuntime;
|
|
|
|
console.log('[Debug] Debug console rendered. Checking DOM...');
|
|
const debugElement = document.querySelector('.ritz-debug');
|
|
console.log('[Debug] Debug element:', debugElement);
|
|
console.log('[Debug] Debug element classes:', debugElement?.className);
|
|
|
|
// Hide the loader status element (we have our own in debug console)
|
|
if (this.statusElement) {
|
|
this.statusElement.style.display = 'none';
|
|
}
|
|
|
|
// Populate runtime stats
|
|
this.updateDebugStats(appComponentName);
|
|
|
|
// Setup keyboard shortcut (Ctrl+`)
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.ctrlKey && e.key === '`') {
|
|
e.preventDefault();
|
|
console.log('[Debug] Toggle debug console');
|
|
this.toggleDebugConsole();
|
|
}
|
|
});
|
|
|
|
// Make debug console and loader globally available
|
|
window.ritzDebugConsole = debugRuntime;
|
|
window.ritzLoader = this;
|
|
|
|
console.log('🎭 Ritz Debug Console loaded! Press Ctrl+` to toggle.');
|
|
|
|
} catch (error) {
|
|
console.warn('Failed to load debug console:', error);
|
|
}
|
|
}
|
|
|
|
toggleDebugConsole() {
|
|
// Toggle the debug console by toggling body class
|
|
// The Ritz debug console component manages its own visibility via signal
|
|
// But we need to trigger the body class for layout
|
|
document.body.classList.toggle('debug-open');
|
|
|
|
// Find the debug console element and toggle its hidden class
|
|
const debugElement = document.querySelector('.ritz-debug');
|
|
if (debugElement) {
|
|
debugElement.classList.toggle('hidden');
|
|
}
|
|
}
|
|
|
|
updateDebugStats(componentName = 'Main') {
|
|
// Update status with component name (inside debug console)
|
|
const statusEl = document.querySelector('#ritz-debug .status');
|
|
if (statusEl) {
|
|
statusEl.textContent = `✅ ${componentName} loaded!`;
|
|
statusEl.className = 'status success';
|
|
}
|
|
|
|
// Update component section
|
|
const componentPre = document.querySelector('#ritz-debug .debug-section:nth-child(1) pre');
|
|
if (componentPre && window.ritzInstance) {
|
|
const wasmSize = window.ritzInstance.exports.memory.buffer.byteLength;
|
|
componentPre.textContent = `${componentName}.wasm (${Math.round(wasmSize / 1024)}KB)`;
|
|
}
|
|
|
|
// Update runtime stats
|
|
const statsPre = document.querySelector('#ritz-debug .debug-section:nth-child(3) pre');
|
|
if (statsPre && window.ritzRuntime && window.ritzInstance) {
|
|
const memory = window.ritzInstance.exports.memory.buffer.byteLength;
|
|
const memoryKB = Math.round(memory / 1024);
|
|
const memoryMB = (memory / 1024 / 1024).toFixed(2);
|
|
const nodeCount = window.ritzRuntime.nodes.size;
|
|
|
|
// Count signals (globals that start with @signal_)
|
|
const exports = Object.keys(window.ritzInstance.exports);
|
|
const signalCount = exports.filter(name =>
|
|
name.startsWith('get_') || name.startsWith('set_')
|
|
).length / 2;
|
|
|
|
statsPre.textContent =
|
|
`Memory: ${memoryKB}KB (${memoryMB}MB)\n` +
|
|
`Signals: ${signalCount} reactive\n` +
|
|
`DOM nodes: ${nodeCount} cached\n` +
|
|
`Log entries: ${window.ritzRuntime.log.length}`;
|
|
}
|
|
}
|
|
|
|
setStatus(message, type = '') {
|
|
if (this.statusElement) {
|
|
this.statusElement.textContent = message;
|
|
this.statusElement.className = 'status ' + type;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Export for use in modules or global scope
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = { RitzRuntime, RitzLoader };
|
|
} else {
|
|
window.RitzRuntime = RitzRuntime;
|
|
window.RitzLoader = RitzLoader;
|
|
}
|