diff --git a/docs/hot-reload.md b/docs/hot-reload.md new file mode 100644 index 0000000000..793fa9fd2d --- /dev/null +++ b/docs/hot-reload.md @@ -0,0 +1,536 @@ +# Hot-Reload for Rust/WASM in Vite Playground + +## Overview + +This document outlines the architecture and implementation for hot-reloading Rust code changes in the BAML Playground Vite app. The goal is to automatically rebuild the WASM module when Rust source files change and display compilation errors in the browser UI. + +### Current Setup + +- **WASM Package**: `engine/baml-schema-wasm` +- **Build Command**: `pnpm build` from `baml-schema-wasm/web` (runs `wasm-pack build ../ --target bundler --out-dir ./web/dist --release`) +- **Vite App**: `typescript/apps/playground` +- **Current Plugin**: `vite-plugin-wasm` for loading WASM modules +- **Requirement**: Must use `--release` flag even in development for acceptable performance + +### Goals + +1. Auto-rebuild WASM when Rust files change +2. Display Cargo compilation errors in browser UI overlay +3. Fast feedback loop for development +4. Minimal dependencies (prefer direct wasm-pack over rsw-rs layer) + +## Architecture Recommendation + +### Recommended Approach: Custom Vite Plugin + Bacon + +**Why not rsw-rs?** +- Adds an extra dependency layer between your build and wasm-pack +- You already have bacon configured (`baml-schema-wasm/bacon.toml`) +- rsw-rs is primarily useful for multi-crate monorepos with complex npm linking needs +- Direct wasm-pack integration is simpler and more maintainable for a single WASM package + +**Why Bacon exports over stderr redirection?** +- Bacon's export system is designed for this use case +- Cleaner configuration with no shell scripting needed +- Automatically formats diagnostics in a structured format +- Works reliably across different platforms and shells +- Can be extended with custom export formats if needed + +**Architecture Components:** + +``` +┌─────────────────┐ +│ Rust Files │ +│ (.rs) │ +└────────┬────────┘ + │ + ↓ +┌─────────────────┐ +│ Bacon Watch │ ← Monitors .rs files +│ (background) │ Runs wasm-pack on changes +└────────┬────────┘ + │ + ↓ +┌─────────────────┐ +│ wasm-pack │ ← Build with --release +│ build │ Outputs to web/dist +└────────┬────────┘ + │ + ↓ +┌─────────────────┐ +│ Bacon Exports │ ← Writes diagnostics to +│ (auto) │ .bacon-diagnostics file +└─────┬───────┬───┘ + │ │ + ↓ ↓ + dist/ .bacon-diagnostics + │ │ + └───┬───┘ + ↓ +┌─────────────────┐ +│ Custom Vite │ ← Watches both files: +│ Plugin │ - dist/ for successful builds +│ │ - .bacon-diagnostics for errors +└────────┬────────┘ + │ + ↓ +┌─────────────────┐ +│ Browser │ ← Reloads on success +│ (HMR/Reload) │ Shows overlay on errors +└─────────────────┘ +``` + +## Implementation Guide + +### Step 1: Set Up Bacon for WASM Building + +You already have a `build-wasm` job in `engine/baml-schema-wasm/bacon.toml`. Update it to use the release flag: + +```toml +[jobs.build-wasm] +command = [ + "sh", "-c", "cd web && pnpm build --release" +] +need_stdout = true +allow_warnings = true +background = false +on_change_strategy = "kill_then_restart" +watch = ["src"] +ignore = ["web/dist", "target"] +``` + +Or create a dedicated watch job: + +```toml +[jobs.watch-wasm] +command = [ + "sh", "-c", "cd web && wasm-pack build ../ --target bundler --out-dir ./web/dist --release" +] +need_stdout = true +allow_warnings = false # We want to capture errors +background = false +on_change_strategy = "kill_then_restart" +watch = ["src"] +ignore = ["web/dist", "target"] +``` + +### Step 2: Create Custom Vite Plugin + +Create a new plugin at `typescript/apps/playground/plugins/vite-plugin-wasm-hmr.ts`: + +```typescript +import type { Plugin, ViteDevServer } from 'vite'; +import { watch } from 'fs'; +import { exec } from 'child_process'; +import * as path from 'path'; +import * as fs from 'fs'; + +interface WasmHmrOptions { + /** Path to the WASM package web directory */ + wasmPackagePath: string; + /** Directory to watch for changes (the dist output) */ + watchPath: string; + /** Optional: bacon config path if you want to start bacon automatically */ + baconConfig?: string; +} + +export function wasmHmr(options: WasmHmrOptions): Plugin { + let server: ViteDevServer; + let distWatcher: ReturnType | null = null; + let diagnosticsWatcher: ReturnType | null = null; + + const wasmDistPath = path.resolve(options.wasmPackagePath, options.watchPath); + const diagnosticsFile = path.resolve(options.wasmPackagePath, '.bacon-diagnostics'); + + let lastBuildHadErrors = false; + + return { + name: 'vite-plugin-wasm-hmr', + enforce: 'pre', + + configureServer(_server) { + server = _server; + + console.log('[wasm-hmr] Watching WASM output:', wasmDistPath); + console.log('[wasm-hmr] Watching diagnostics:', diagnosticsFile); + + // Watch the bacon diagnostics file + diagnosticsWatcher = watch(diagnosticsFile, async (eventType) => { + if (eventType !== 'change') return; + + console.log('[wasm-hmr] Build completed, checking diagnostics...'); + + // Read diagnostics file + if (!fs.existsSync(diagnosticsFile)) { + lastBuildHadErrors = false; + return; + } + + const diagnostics = fs.readFileSync(diagnosticsFile, 'utf-8'); + const hasErrors = diagnostics.split('\n').some(line => line.trim().startsWith('error')); + + if (hasErrors) { + lastBuildHadErrors = true; + console.log('[wasm-hmr] Build failed with errors'); + + // Send error overlay to browser + server.ws.send({ + type: 'error', + err: { + message: 'Rust compilation failed', + stack: formatBaconDiagnostics(diagnostics), + plugin: 'vite-plugin-wasm-hmr', + }, + }); + } else { + // No errors - if we previously had errors, clear them + if (lastBuildHadErrors) { + console.log('[wasm-hmr] Build succeeded, clearing previous errors'); + lastBuildHadErrors = false; + } + // Success will trigger reload when dist files change + } + }); + + // Watch the WASM dist folder for changes + distWatcher = watch(wasmDistPath, { recursive: true }, async (eventType, filename) => { + if (!filename) return; + + // Only trigger on .js or .wasm file changes + if (filename.endsWith('.js') || filename.endsWith('.wasm')) { + console.log('[wasm-hmr] WASM package rebuilt:', filename); + + // Only reload if we don't have errors + if (!lastBuildHadErrors) { + server.ws.send({ + type: 'full-reload', + path: '*', + }); + console.log('[wasm-hmr] Triggering browser reload'); + } + } + }); + + server.httpServer?.on('close', () => { + distWatcher?.close(); + diagnosticsWatcher?.close(); + }); + }, + + handleHotUpdate({ file }) { + // If a .rs file changes, we don't handle it directly + // (bacon handles the rebuild), but we can notify the user + if (file.endsWith('.rs')) { + console.log('[wasm-hmr] Rust file changed:', file); + console.log('[wasm-hmr] Waiting for bacon to rebuild...'); + } + return []; + }, + }; +} + +/** + * Format bacon diagnostics output for Vite's error overlay + */ +function formatBaconDiagnostics(diagnostics: string): string { + const lines = diagnostics.split('\n').filter(line => line.trim()); + + if (lines.length === 0) { + return 'Build failed with unknown errors'; + } + + const formatted: string[] = ['Rust Compilation Errors:\n']; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + // Parse bacon format: {kind} {path}:{line}:{column} {message} + if (trimmed.startsWith('error')) { + formatted.push(`\n❌ ${trimmed}\n`); + } else if (trimmed.startsWith('warning')) { + formatted.push(`\n⚠️ ${trimmed}\n`); + } else { + formatted.push(` ${trimmed}`); + } + } + + return formatted.join('\n'); +} +``` + +### Step 3: Configure Bacon to Export Diagnostics + +Update your bacon config to use bacon's built-in exports feature to capture build diagnostics: + +```toml +[jobs.watch-wasm-dev] +command = [ + "sh", "-c", + "cd web && wasm-pack build ../ --target bundler --out-dir ./web/dist --release" +] +need_stdout = true +allow_warnings = true +background = false +on_change_strategy = "kill_then_restart" +watch = ["src"] +ignore = ["web/dist", "target"] + +# Export build diagnostics for Vite plugin to display +[exports.wasm-build-status] +auto = true +path = "web/.bacon-diagnostics" +line_format = "{kind} {path}:{line}:{column} {message}" +``` + +This uses bacon's automatic exports to write diagnostics to a file that the Vite plugin watches. + +### Step 4: Update Vite Config + +Modify `typescript/apps/playground/vite.config.ts`: + +```typescript +import { wasmHmr } from './plugins/vite-plugin-wasm-hmr'; + +export default defineConfig({ + plugins: [ + react({ + babel: { + presets: ['jotai/babel/preset'], + }, + }), + wasm(), + wasmHmr({ + wasmPackagePath: path.resolve(__dirname, '../../../engine/baml-schema-wasm/web'), + watchPath: 'dist', + }), + // ... other plugins + ], + // ... rest of config +}); +``` + +### Step 5: Development Workflow + +1. **Terminal 1** - Start Bacon watcher: + ```bash + cd engine/baml-schema-wasm + bacon watch-wasm-dev + ``` + +2. **Terminal 2** - Start Vite dev server: + ```bash + cd typescript/apps/playground + pnpm dev + ``` + +Now when you edit Rust files: +1. Bacon detects the change and triggers `wasm-pack build --release` +2. On success: New WASM files are written to `web/dist/` +3. Vite plugin detects dist changes and triggers browser reload +4. On error: Error is written to file, plugin reads it and shows overlay + +## Alternative Approaches + +### Option 2: Use rsw-rs (Not Recommended) + +If you decide to use rsw-rs despite the extra layer: + +1. Install rsw-rs globally: `cargo install rsw` +2. Install vite-plugin: `pnpm add -D vite-plugin-rsw` +3. Create `rsw.toml` in project root: + ```toml + [[crates]] + name = "baml-schema-build" + path = "engine/baml-schema-wasm" + target = "bundler" + out-dir = "engine/baml-schema-wasm/web/dist" + profile = "release" # Even for dev mode + ``` +4. Update vite.config.ts: + ```typescript + import ViteRsw from 'vite-plugin-rsw'; + + export default defineConfig({ + plugins: [ + ViteRsw({ + crates: ["baml-schema-build"], + profile: "release", + }), + // ... other plugins + ], + }); + ``` + +**Tradeoffs:** +- ✅ Automatic error overlay (built-in) +- ✅ No need to run bacon separately +- ❌ Extra dependency to maintain +- ❌ Less control over build process +- ❌ May not work well with existing bacon setup + +### Option 3: Manual Watch Script + +Create a Node.js watch script that directly watches Rust files: + +```typescript +// scripts/watch-wasm.ts +import { watch } from 'chokidar'; +import { exec } from 'child_process'; +import debounce from 'lodash.debounce'; + +const watcher = watch('engine/baml-schema-wasm/src/**/*.rs', { + ignored: /target/, + persistent: true, +}); + +const rebuild = debounce(() => { + console.log('Rebuilding WASM...'); + exec( + 'cd engine/baml-schema-wasm/web && pnpm build', + (error, stdout, stderr) => { + if (error) { + console.error('Build failed:', stderr); + } else { + console.log('Build successful!'); + } + } + ); +}, 300); + +watcher.on('change', rebuild); +``` + +**Tradeoffs:** +- ✅ Simple, direct control +- ✅ No extra rust dependencies +- ❌ Manual error overlay implementation needed +- ❌ Another process to run +- ❌ Less robust than bacon + +## Optimizations + +### 1. Incremental Builds + +Ensure cargo uses incremental compilation for faster rebuilds: + +```toml +# In Cargo.toml or .cargo/config.toml +[profile.release] +incremental = true +``` + +### 2. Parallel Compilation + +Set cargo to use more CPU cores: + +```bash +# In your shell profile or .envrc +export CARGO_BUILD_JOBS=8 +``` + +### 3. Cache wasm-pack artifacts + +Make sure the `target/` directory is preserved between builds (not cleaned). + +### 4. Debounce File Changes + +The Vite plugin example above already handles rapid file changes, but you can adjust the debounce timing if needed. + +## Troubleshooting + +### WASM module fails to reload + +**Symptom**: Changes to Rust code don't appear in browser even after rebuild. + +**Solutions**: +1. Check browser console for WASM loading errors +2. Hard refresh (Cmd+Shift+R) to clear WASM cache +3. Verify dist files are actually updating: `ls -la engine/baml-schema-wasm/web/dist/` +4. Check that Vite alias points to correct dist folder (see vite.config.ts lines 57-64) + +### Build errors not showing in overlay + +**Symptom**: Rust compilation fails but no error overlay appears. + +**Solutions**: +1. Check that error file is being written: `cat engine/baml-schema-wasm/target/wasm-build-error.txt` +2. Verify Vite plugin is loaded: check console for `[wasm-hmr]` messages +3. Check WebSocket connection in browser DevTools Network tab +4. Try sending a manual error via Vite's WebSocket to test overlay + +### Slow rebuild times + +**Symptom**: Each Rust change takes >30 seconds to rebuild. + +**Solutions**: +1. Ensure incremental compilation is enabled (see Optimizations) +2. Use `--timings` flag to see where time is spent: `wasm-pack build --timings` +3. Consider using `--dev` for non-release builds (but note performance impact) +4. Check if you have enough RAM (WASM builds can be memory-intensive) +5. Use `sccache` or `cargo-chef` for better caching + +### Bacon not detecting changes + +**Symptom**: Editing Rust files doesn't trigger bacon rebuild. + +**Solutions**: +1. Check `watch = ["src"]` paths in bacon.toml +2. Verify files aren't in `ignore` list +3. Try running `bacon --debug` to see what's being watched +4. Ensure your editor saves files properly (some save to temp files) + +## Testing + +### Test the Complete Flow + +1. Start bacon: `cd engine/baml-schema-wasm && bacon watch-wasm-dev` +2. Start Vite: `cd typescript/apps/playground && pnpm dev` +3. Open browser to `http://localhost:3030` +4. Edit a Rust file in `engine/baml-schema-wasm/src/` +5. Verify: + - [ ] Bacon detects change and starts rebuild + - [ ] wasm-pack completes successfully + - [ ] Vite plugin detects dist change + - [ ] Browser automatically reloads + - [ ] Changes appear in the app + +### Test Error Handling + +1. Introduce a syntax error in a Rust file (e.g., remove a semicolon) +2. Save the file +3. Verify: + - [ ] Bacon shows compilation error + - [ ] Error is written to file + - [ ] Vite plugin reads error + - [ ] Browser shows error overlay with formatted Rust error + - [ ] Error overlay is readable and helpful + +### Test Recovery + +1. After introducing an error (above), fix it +2. Save the file +3. Verify: + - [ ] Bacon rebuilds successfully + - [ ] Error file is removed + - [ ] Browser automatically reloads + - [ ] Error overlay disappears + - [ ] App works correctly + +## Future Improvements + +1. **Faster Builds**: Explore wasm-pack alternatives like `wasm-bindgen-cli` directly +2. **Granular HMR**: Investigate if partial WASM module reloading is possible +3. **Build Notifications**: Add desktop notifications for build completion/errors +4. **Metrics**: Track and display build times in the UI +5. **Auto-recovery**: Automatically retry failed builds on next file change +6. **Source Maps**: Improve Rust source map support for better debugging + +## Resources + +- [wasm-pack documentation](https://rustwasm.github.io/wasm-pack/) +- [Bacon configuration guide](https://dystroy.org/bacon/config/) +- [Vite Plugin API](https://vitejs.dev/guide/api-plugin.html) +- [Vite HMR API](https://vitejs.dev/guide/api-hmr.html) +- [rsw-rs repository](https://github.com/rwasm/rsw-rs) (for reference) +- [vite-plugin-rsw](https://github.com/rwasm/vite-plugin-rsw) (for reference) diff --git a/engine/baml-runtime/src/tracingv2/publisher/publisher.rs b/engine/baml-runtime/src/tracingv2/publisher/publisher.rs index ed08bda563..a9c43e3b2d 100644 --- a/engine/baml-runtime/src/tracingv2/publisher/publisher.rs +++ b/engine/baml-runtime/src/tracingv2/publisher/publisher.rs @@ -228,7 +228,7 @@ pub fn start_publisher( return; } if lookup.env_var("BOUNDARY_API_KEY").is_none() { - log::debug!("Skipping publisher because BOUNDARY_API_KEY is not set"); + log::debug!("Skipping publisher because BOUNDARY_API_KEY is not set.---exp3"); return; } log::debug!("Starting publisher"); diff --git a/engine/baml-schema-wasm/.gitignore b/engine/baml-schema-wasm/.gitignore index 413b5ac362..a888b36f7d 100644 --- a/engine/baml-schema-wasm/.gitignore +++ b/engine/baml-schema-wasm/.gitignore @@ -1,3 +1,8 @@ dist/src/* nodejs/src/* web/src/* + +# Hot-reload temporary files +web/.wasm-build.lock +web/.wasm-build-status +web/.wasm-build-output.tmp diff --git a/engine/baml-schema-wasm/bacon.toml b/engine/baml-schema-wasm/bacon.toml new file mode 100644 index 0000000000..d56dbbc7bd --- /dev/null +++ b/engine/baml-schema-wasm/bacon.toml @@ -0,0 +1,139 @@ +# This is a configuration file for the bacon tool +# +# Complete help on configuration: https://dystroy.org/bacon/config/ +# +# You may check the current default at +# https://github.com/Canop/bacon/blob/main/defaults/default-bacon.toml + +default_job = "check" +env.CARGO_TERM_COLOR = "always" + +[jobs.check] +command = ["cargo", "check"] +need_stdout = false + +[jobs.check-all] +command = ["cargo", "check", "--all-targets"] +need_stdout = false + +# Run clippy on the default target +[jobs.clippy] +command = ["cargo", "clippy"] +need_stdout = false + +# Run clippy on all targets +# To disable some lints, you may change the job this way: +# [jobs.clippy-all] +# command = [ +# "cargo", "clippy", +# "--all-targets", +# "--", +# "-A", "clippy::bool_to_int_with_if", +# "-A", "clippy::collapsible_if", +# "-A", "clippy::derive_partial_eq_without_eq", +# ] +# need_stdout = false +[jobs.clippy-all] +command = ["cargo", "clippy", "--all-targets"] +need_stdout = false + +# This job lets you run +# - all tests: bacon test +# - a specific test: bacon test -- config::test_default_files +# - the tests of a package: bacon test -- -- -p config +[jobs.test] +command = ["cargo", "test"] +need_stdout = true + +[jobs.nextest] +command = [ + "cargo", "nextest", "run", + "--hide-progress-bar", "--failure-output", "final" +] +need_stdout = true +analyzer = "nextest" + +[jobs.doc] +command = ["cargo", "doc", "--no-deps"] +need_stdout = false + +# If the doc compiles, then it opens in your browser and bacon switches +# to the previous job +[jobs.doc-open] +command = ["cargo", "doc", "--no-deps", "--open"] +need_stdout = false +on_success = "back" # so that we don't open the browser at each change + +# You can run your application and have the result displayed in bacon, +# if it makes sense for this crate. +[jobs.run] +command = [ + "cargo", "run", + # put launch parameters for your program behind a `--` separator +] +need_stdout = true +allow_warnings = true +background = true + +# Run your long-running application (eg server) and have the result displayed in bacon. +# For programs that never stop (eg a server), `background` is set to false +# to have the cargo run output immediately displayed instead of waiting for +# program's end. +# 'on_change_strategy' is set to `kill_then_restart` to have your program restart +# on every change (an alternative would be to use the 'F5' key manually in bacon). +# If you often use this job, it makes sense to override the 'r' key by adding +# a binding `r = job:run-long` at the end of this file . +# A custom kill command such as the one suggested below is frequently needed to kill +# long running programs (uncomment it if you need it) +[jobs.run-long] +command = [ + "cargo", "run", + # put launch parameters for your program behind a `--` separator +] +need_stdout = true +allow_warnings = true +background = false +on_change_strategy = "kill_then_restart" +# kill = ["pkill", "-TERM", "-P"] + +# [jobs.build-wasm] +# command = [ +# # "sh", "-c", "sleep 1 && exec cargo run --bin http-server", +# "pnpm build", +# # put launch parameters for your program behind a `--` separator +# ] +# need_stdout = true +# allow_warnings = true +# background = false +# on_change_strategy = "kill_then_restart" +# watch = ["rust-src"] +# ignore = ["baml/engine/baml-rpc/bindings"] + +# Hot-reload development job for Vite playground +# Runs watch-build.py script to build WASM with file locking +[jobs.watch-wasm-dev] +command = ["./watch-build.py"] +need_stdout = true +allow_warnings = true +background = false +on_change_strategy = "kill_then_restart" +watch = ["src"] +ignore = ["web/dist", "target"] + + +# This parameterized job runs the example of your choice, as soon +# as the code compiles. +# Call it as +# bacon ex -- my-example +[jobs.ex] +command = ["cargo", "run", "--example"] +need_stdout = true +allow_warnings = true + +# You may define here keybindings that would be specific to +# a project, for example a shortcut to launch a specific job. +# Shortcuts to internal functions (scrolling, toggling, etc.) +# should go in your personal global prefs.toml file instead. +[keybindings] +# alt-m = "job:my-job" +c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target diff --git a/engine/baml-schema-wasm/src/abort_controller.rs b/engine/baml-schema-wasm/src/abort_controller.rs index b3a3f1444d..e32ecae440 100644 --- a/engine/baml-schema-wasm/src/abort_controller.rs +++ b/engine/baml-schema-wasm/src/abort_controller.rs @@ -4,7 +4,7 @@ use baml_runtime::TripWire; use stream_cancel::Trigger; use wasm_bindgen::prelude::*; use web_sys::AbortSignal; - +// ... thread_local! { static ABORT_CLOSURES: RefCell>> = RefCell::new(HashMap::new()); static OPERATION_TRIGGERS: RefCell> = RefCell::new(HashMap::new()); diff --git a/engine/baml-schema-wasm/src/runtime_wasm/mod.rs b/engine/baml-schema-wasm/src/runtime_wasm/mod.rs index a37cc9309f..e1fd95c977 100644 --- a/engine/baml-schema-wasm/src/runtime_wasm/mod.rs +++ b/engine/baml-schema-wasm/src/runtime_wasm/mod.rs @@ -62,6 +62,7 @@ pub fn on_wasm_init() { // this is disabled by default because its slows down release mode builds. cfg_if::cfg_if! { if #[cfg(debug_assertions)] { + const LOG_LEVEL: log::Level = log::Level::Debug; } else { const LOG_LEVEL: log::Level = log::Level::Debug; @@ -71,10 +72,10 @@ pub fn on_wasm_init() { //wasm_logger::init(wasm_logger::Config::new(LOG_LEVEL)); match console_log::init_with_level(LOG_LEVEL) { Ok(_) => web_sys::console::log_1( - &format!("Initialized BAML runtime logging as log::{LOG_LEVEL}").into(), + &format!("Initialized BAML runtime logging as log::{LOG_LEVEL}...").into(), ), Err(e) => web_sys::console::log_1( - &format!("Failed to initialize BAML runtime logging: {e:?}").into(), + &format!("Failed to initialize BAML runtime logging: {e:?}.").into(), ), } diff --git a/engine/baml-schema-wasm/watch-build.py b/engine/baml-schema-wasm/watch-build.py new file mode 100755 index 0000000000..490b8259e4 --- /dev/null +++ b/engine/baml-schema-wasm/watch-build.py @@ -0,0 +1,153 @@ +#!/usr/bin/env -S uv run --quiet --script +# /// script +# requires-python = ">=3.11" +# /// +""" +Hot-reload build script for WASM development. +Called by bacon when Rust source files change. +""" + +import fcntl +import os +import subprocess +import sys +import signal +from pathlib import Path + +# Configuration +SCRIPT_DIR = Path(__file__).parent +LOCK_FILE = SCRIPT_DIR / "web" / ".wasm-build.lock" +STATUS_FILE = SCRIPT_DIR / "web" / ".wasm-build-status" +OUTPUT_FILE = SCRIPT_DIR / "web" / ".wasm-build-output.tmp" + +# Global state for cleanup +lock_fd = None +should_cleanup = True + + +def write_status(status: str): + """Write status and flush immediately.""" + STATUS_FILE.write_text(status) + # Force filesystem sync + os.sync() + + +def cleanup(signum=None, frame=None): + """Clean up resources on exit.""" + global should_cleanup + + if not should_cleanup: + return + + print("Cleaning up...", file=sys.stderr) + + # If interrupted while refreshing, mark as cancelled + if STATUS_FILE.exists() and STATUS_FILE.read_text().strip() == "refreshing": + write_status("cancelled") + print("Status: Build cancelled", file=sys.stderr) + + # Remove temp files + if OUTPUT_FILE.exists(): + OUTPUT_FILE.unlink() + + # Release lock (will happen automatically, but being explicit) + if lock_fd is not None: + try: + fcntl.flock(lock_fd, fcntl.LOCK_UN) + os.close(lock_fd) + except: + pass + + print("Cleanup complete", file=sys.stderr) + should_cleanup = False + + +def main(): + global lock_fd + + # Change to script directory + os.chdir(SCRIPT_DIR) + + # Set up signal handlers + signal.signal(signal.SIGINT, cleanup) + signal.signal(signal.SIGTERM, cleanup) + + # Create lock file if it doesn't exist + LOCK_FILE.parent.mkdir(parents=True, exist_ok=True) + LOCK_FILE.touch() + + # Try to acquire exclusive lock (non-blocking) + try: + lock_fd = os.open(str(LOCK_FILE), os.O_RDWR | os.O_CREAT) + fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + print("Another build is in progress, skipping...", file=sys.stderr) + return 0 + + print("Lock acquired, running build...", file=sys.stderr) + + # Signal that build is starting + write_status("refreshing") + print("Status: Build starting (refreshing)", file=sys.stderr) + + # Run wasm-pack build + # Note: Using --release even for dev because --dev bundles are too slow + cmd = [ + "wasm-pack", + "build", + "./", + "--target", + "bundler", + "--out-dir", + "./web/dist", + # cant use --dev until we solve this issue: https://github.com/wasm-bindgen/wasm-bindgen/issues/1563 + "--dev", + ] + + # Run process and capture output while displaying it + with open(OUTPUT_FILE, "w") as output_f: + process = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1 + ) + + # Stream output to both stdout and file + for line in process.stdout: + print(line, end="") + output_f.write(line) + + process.wait() + exit_code = process.returncode + + print(f"Build exit code: {exit_code}", file=sys.stderr) + + # Write status based on exit code + if exit_code == 0: + write_status("success") + print("=" * 47, file=sys.stderr) + print("Status: Build succeeded (success written to file)", file=sys.stderr) + print("=" * 47, file=sys.stderr) + + # Verify + verify_status = STATUS_FILE.read_text().strip() + print(f"Verified: status file contains: '{verify_status}'", file=sys.stderr) + else: + # Build failed - save output for Vite error overlay + error_output = OUTPUT_FILE.read_text() + write_status(error_output) + print("=" * 47, file=sys.stderr) + print("Status: Build failed (errors written)", file=sys.stderr) + print("=" * 47, file=sys.stderr) + + # Clean up + cleanup() + + return exit_code + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + cleanup() + sys.exit(1) diff --git a/engine/baml-schema-wasm/web/.bacon-diagnostics b/engine/baml-schema-wasm/web/.bacon-diagnostics new file mode 100644 index 0000000000..4ea3542234 --- /dev/null +++ b/engine/baml-schema-wasm/web/.bacon-diagnostics @@ -0,0 +1,394 @@ +[INFO]: 🎯 Checking for the Wasm target... +[INFO]: 🌀 Compiling to Wasm... + Blocking waiting for file lock on build directory +warning: unused variable: `w` + --> baml-compiler/src/hir/dump.rs:305:29 + | +305 |  if let Some(w) = when { + | ^ help: if this is intentional, prefix it with an underscore: `_w` + | + = note: `#[warn(unused_variables)]` on by default + +warning: unused variable: `watch_options` + --> baml-compiler/src/hir/lowering.rs:462:5 + | +462 |  watch_options: &HashMap, Option)>, + | ^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_watch_options` + +warning: unreachable pattern + --> baml-compiler/src/thir/typecheck.rs:144:13 + | +109 |  "baml.String.length" => TypeIR::arrow(vec![TypeIR::string()], TypeIR::int()), + | -------------------- matches all the relevant values +... +144 |  "baml.String.length" => TypeIR::arrow(vec![TypeIR::string()], TypeIR::int()), + | ^^^^^^^^^^^^^^^^^^^^ no value can reach this + | + = note: `#[warn(unreachable_patterns)]` on by default + +warning: unused variable: `var_name` + --> baml-compiler/src/thir/typecheck.rs:2571:30 + | +2571 |  if let Some((var_name, excluded_type)) = else_narrowing { + | ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_var_name` + +warning: unused variable: `excluded_type` + --> baml-compiler/src/thir/typecheck.rs:2571:40 + | +2571 |  if let Some((var_name, excluded_type)) = else_narrowing { + | ^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_excluded_type` + +warning: function `assign_error` is never used + --> baml-compiler/src/thir/typecheck.rs:1444:4 + | +1444 | fn assign_error(lhs: &thir::Expr) -> Cow<'static, str> { + | ^^^^^^^^^^^^ + | + = note: `#[warn(dead_code)]` on by default + +warning: function `is_assignable` is never used + --> baml-compiler/src/thir/typecheck.rs:1465:4 + | +1465 | fn is_assignable( + | ^^^^^^^^^^^^^ + +warning: `baml-compiler` (lib) generated 7 warnings +warning: unused variable: `output_path` + --> generators/utils/dir_writer/src/lib.rs:407:30 + | +407 |  pub fn commit(&mut self, output_path: &Path) -> Result> { + | ^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_output_path` + | + = note: `#[warn(unused_variables)]` on by default + +warning: field `lang` is never read + --> generators/utils/dir_writer/src/lib.rs:220:5 + | +217 | pub struct FileCollector<'a, L: LanguageFeatures + Default> { + | ------------- field in this struct +... +220 |  lang: L, + | ^^^^ + | + = note: `#[warn(dead_code)]` on by default + +warning: function `try_delete_tmp_dir` is never used + --> generators/utils/dir_writer/src/lib.rs:226:4 + | +226 | fn try_delete_tmp_dir(temp_path: &Path) -> Result<()> { + | ^^^^^^^^^^^^^^^^^^ + +warning: method `remove_dir_safe` is never used + --> generators/utils/dir_writer/src/lib.rs:326:8 + | +274 | impl<'a, L: LanguageFeatures + Default> FileCollector<'a, L> { + | ------------------------------------------------------------ method in this implementation +... +326 |  fn remove_dir_safe(&self, output_path: &Path) -> Result<()> { + | ^^^^^^^^^^^^^^^ + +warning: `dir-writer` (lib) generated 4 warnings +warning: field `docstring` is never read + --> generators/languages/go/src/generated_types.rs:78:13 + | +75 |  pub struct UnionGo<'a> { + | ------- field in this struct +... +78 |  pub docstring: Option, + | ^^^^^^^^^ + | + = note: `#[warn(dead_code)]` on by default + +warning: associated items `current` and `types_builder` are never used + --> generators/languages/go/src/package.rs:34:12 + | +9 | impl Package { + | ------------ associated items in this implementation +... +34 |  pub fn current(&self) -> String { + | ^^^^^^^ +... +46 |  pub fn types_builder() -> Package { + | ^^^^^^^^^^^^^ + +warning: method `is_optional` is never used + --> generators/languages/go/src/type.rs:46:12 + | +45 | impl TypeMetaGo { + | --------------- method in this implementation +46 |  pub fn is_optional(&self) -> bool { + | ^^^^^^^^^^^ + +warning: `generators-go` (lib) generated 3 warnings +warning: field `documentation` is never read + --> generators/languages/ruby/src/functions.rs:11:16 + | +10 | pub struct FunctionRb { + | ---------- field in this struct +11 |  pub(crate) documentation: Option, + | ^^^^^^^^^^^^^ + | + = note: `#[warn(dead_code)]` on by default + +warning: struct `Parser` is never constructed + --> generators/languages/ruby/src/functions.rs:40:8 + | +40 | struct Parser<'a> { + | ^^^^^^ + +warning: function `render_parser` is never used + --> generators/languages/ruby/src/functions.rs:45:8 + | +45 | pub fn render_parser( + | ^^^^^^^^^^^^^ + +warning: struct `TypeMap` is never constructed + --> generators/languages/ruby/src/functions.rs:71:8 + | +71 | struct TypeMap<'a> { + | ^^^^^^^ + +warning: function `render_type_map` is never used + --> generators/languages/ruby/src/functions.rs:76:8 + | +76 | pub fn render_type_map(classes: &[ClassRb], enums: &[EnumRb]) -> Result { + | ^^^^^^^^^^^^^^^ + +warning: struct `SourceFiles` is never constructed + --> generators/languages/ruby/src/functions.rs:96:8 + | +96 | struct SourceFiles<'a> { + | ^^^^^^^^^^^ + +warning: function `render_source_files` is never used + --> generators/languages/ruby/src/functions.rs:100:8 + | +100 | pub fn render_source_files(file_map: Vec<(String, String)>) -> Result { + | ^^^^^^^^^^^^^^^^^^^ + +warning: function `render_config` is never used + --> generators/languages/ruby/src/functions.rs:119:8 + | +119 | pub fn render_config(_pkg: &CurrentRenderPackage) -> Result { + | ^^^^^^^^^^^^^ + +warning: function `render_tracing` is never used + --> generators/languages/ruby/src/functions.rs:123:8 + | +123 | pub fn render_tracing(_pkg: &CurrentRenderPackage) -> Result { + | ^^^^^^^^^^^^^^ + +warning: struct `Init` is never constructed + --> generators/languages/ruby/src/functions.rs:130:8 + | +130 | struct Init<'a> { + | ^^^^ + +warning: function `render_init` is never used + --> generators/languages/ruby/src/functions.rs:135:8 + | +135 | pub fn render_init( + | ^^^^^^^^^^^ + +warning: field `pkg` is never read + --> generators/languages/ruby/src/generated_types.rs:386:5 + | +385 | struct RbTypesUtils<'a> { + | ------------ field in this struct +386 |  pkg: &'a CurrentRenderPackage, + | ^^^ + +warning: field `pkg` is never read + --> generators/languages/ruby/src/generated_types.rs:444:5 + | +443 | pub struct RbStreamTypesUtils<'a> { + | ------------------ field in this struct +444 |  pkg: &'a CurrentRenderPackage, + | ^^^ + +warning: field `pkg` is never read + --> generators/languages/ruby/src/generated_types.rs:30:13 + | +25 |  pub struct ClassRb<'a> { + | ------- field in this struct +... +30 |  pub pkg: &'a CurrentRenderPackage, + | ^^^ + +warning: method `to_type_builder_property` is never used + --> generators/languages/ruby/src/generated_types.rs:110:16 + | +109 |  impl super::EnumRb { + | ------------------ method in this implementation +110 |  pub fn to_type_builder_property(&self) -> TypeBuilderProperty<'_, Self> { + | ^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: method `to_type_builder_property` is never used + --> generators/languages/ruby/src/generated_types.rs:129:16 + | +128 |  impl super::ClassRb<'_> { + | ----------------------- method in this implementation +129 |  pub fn to_type_builder_property(&self) -> TypeBuilderProperty<'_, Self> { + | ^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: method `to_type_builder_object` is never used + --> generators/languages/ruby/src/generated_types.rs:240:16 + | +215 |  impl<'a> super::ClassRb<'a> { + | --------------------------- method in this implementation +... +240 |  pub fn to_type_builder_object(&'a self) -> TypeBuilderClassObject<'a> { + | ^^^^^^^^^^^^^^^^^^^^^^ + +warning: method `to_type_builder_object` is never used + --> generators/languages/ruby/src/generated_types.rs:336:16 + | +311 |  impl<'a> super::EnumRb { + | ---------------------- method in this implementation +... +336 |  pub fn to_type_builder_object(&'a self) -> TypeBuilderEnumObject<'a> { + | ^^^^^^^^^^^^^^^^^^^^^^ + +warning: method `in_type_definition` is never used + --> generators/languages/ruby/src/package.rs:34:12 + | +11 | impl Package { + | ------------ method in this implementation +... +34 |  pub fn in_type_definition(&self) -> bool { + | ^^^^^^^^^^^^^^^^^^ + +warning: method `name` is never used + --> generators/languages/ruby/src/package.rs:128:12 + | +81 | impl CurrentRenderPackage { + | ------------------------- method in this implementation +... +128 |  pub fn name(&self) -> String { + | ^^^^ + +warning: method `is_checked` is never used + --> generators/languages/ruby/src/type.rs:28:12 + | +23 | impl TypeMetaRb { + | --------------- method in this implementation +... +28 |  pub fn is_checked(&self) -> bool { + | ^^^^^^^^^^ + +warning: associated function `new` is never used + --> generators/languages/ruby/src/type.rs:95:12 + | +94 | impl EscapedRubyString { + | ---------------------- associated function in this implementation +95 |  pub fn new(s: &str) -> Self { + | ^^^ + +warning: method `name` is never used + --> generators/languages/python/src/package.rs:126:12 + | +82 | impl CurrentRenderPackage { + | ------------------------- method in this implementation +... +126 |  pub fn name(&self) -> String { + | ^^^^ + | + = note: `#[warn(dead_code)]` on by default + +warning: field `stream_type` is never read + --> generators/languages/python/src/watchers.rs:28:9 + | +24 | pub struct VarEventPy { + | ---------- field in this struct +... +28 |  pub stream_type: String, + | ^^^^^^^^^^^ + | + = note: `VarEventPy` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis + +warning: field `class_name` is never read + --> generators/languages/python/src/watchers.rs:36:9 + | +32 | pub struct ChildCollectorPy { + | ---------------- field in this struct +... +36 |  pub class_name: String, + | ^^^^^^^^^^ + | + = note: `ChildCollectorPy` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis + +warning: field `pkg` is never read + --> generators/languages/typescript/src/functions.rs:151:5 + | +150 | struct Tracing<'a> { + | ------- field in this struct +151 |  pkg: &'a CurrentRenderPackage, + | ^^^ + | + = note: `#[warn(dead_code)]` on by default + +warning: field `pkg` is never read + --> generators/languages/typescript/src/functions.rs:257:5 + | +254 | struct ReactServerStreamingTypes<'a> { + | ------------------------- field in this struct +... +257 |  pkg: &'a CurrentRenderPackage, + | ^^^ + +warning: function `ir_expr_fn_to_ts` is never used + --> generators/languages/typescript/src/ir_to_ts/functions.rs:36:8 + | +36 | pub fn ir_expr_fn_to_ts(function: &ExprFunctionNode, pkg: &CurrentRenderPackage) -> FunctionTS { + | ^^^^^^^^^^^^^^^^ + +warning: method `name` is never used + --> generators/languages/typescript/src/package.rs:96:12 + | +67 | impl CurrentRenderPackage { + | ------------------------- method in this implementation +... +96 |  pub fn name(&self) -> String { + | ^^^^ + +warning: variant `Interface` is never constructed + --> generators/languages/typescript/src/type.rs:148:5 + | +117 | pub enum TypeTS { + | ------ variant in this enum +... +148 |  Interface { + | ^^^^^^^^^ + | + = note: `TypeTS` has derived impls for the traits `Debug` and `Clone`, but these are intentionally ignored during dead code analysis + +warning: method `default_name_within_union` is never used + --> generators/languages/typescript/src/type.rs:162:12 + | +160 | impl TypeTS { + | ----------- method in this implementation +161 |  // for unions, we need a default name for the type when the union is not named +162 |  pub fn default_name_within_union(&self) -> String { + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: `generators-ruby` (lib) generated 22 warnings +warning: `generators-python` (lib) generated 3 warnings +warning: `generators-typescript` (lib) generated 6 warnings + Compiling baml-schema-build v0.211.2 (/Users/aaronvillalpando/Projects/baml/engine/baml-schema-wasm) +error[E0599]: no variant or associated item named `Debu` found for enum `Level` in the current scope + --> baml-schema-wasm/src/runtime_wasm/mod.rs:65:55 + | +65 |  const LOG_LEVEL: log::Level = log::Level::Debu; + | ^^^^ variant or associated item not found in `Level` + | +help: there is a variant with a similar name + | +65 |  const LOG_LEVEL: log::Level = log::Level::Debug; + | + + +For more information about this error, try `rustc --explain E0599`. +error: could not compile `baml-schema-build` (lib) due to 1 previous error +Error: Compiling your crate to WebAssembly failed +Caused by: Compiling your crate to WebAssembly failed +Caused by: failed to execute `cargo build`: exited with exit status: 101 + full command: cd "../" && "cargo" "build" "--lib" "--target" "wasm32-unknown-unknown" diff --git a/engine/baml-schema-wasm/web/.wasm-build-debounce b/engine/baml-schema-wasm/web/.wasm-build-debounce new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mise.toml b/mise.toml index 780ebf1022..80e1e0b1fb 100644 --- a/mise.toml +++ b/mise.toml @@ -14,6 +14,9 @@ java = "temurin-23" "cargo:cargo-watch" = "latest" "cargo:wasm-pack" = "0.13.1" "cargo:ripgrep" = "latest" +# Makes the wasm-pack build slightly faster by not +# having wasm-pack redownload everything again. +"cargo:wasm-bindgen-cli" = "0.2.101" # Go tools # locked to match the version in .github/actions/setup-go/action.yml diff --git a/typescript/apps/fiddle-web-app/app/embed/clientwrapper.tsx b/typescript/apps/fiddle-web-app/app/embed/clientwrapper.tsx index e0a6e0cd68..5ff3b7c77f 100644 --- a/typescript/apps/fiddle-web-app/app/embed/clientwrapper.tsx +++ b/typescript/apps/fiddle-web-app/app/embed/clientwrapper.tsx @@ -143,6 +143,7 @@ function EmbedComponentInner({ files }: EmbedComponentProps) { return (
+
{/*

This is an embeddable React Component!

*/} diff --git a/typescript/apps/playground/plugins/vite-plugin-wasm-hmr.ts b/typescript/apps/playground/plugins/vite-plugin-wasm-hmr.ts new file mode 100644 index 0000000000..5eb6ba6521 --- /dev/null +++ b/typescript/apps/playground/plugins/vite-plugin-wasm-hmr.ts @@ -0,0 +1,248 @@ +import type { Plugin, ViteDevServer } from 'vite'; +import { watch } from 'fs'; +import * as path from 'path'; +import * as fs from 'fs'; + +interface WasmHmrOptions { + /** Path to the WASM package web directory */ + wasmPackagePath: string; + /** Directory to watch for changes (the dist output) */ + watchPath: string; + /** Local path to copy WASM files to for HMR */ + localCopyPath?: string; +} + +export function wasmHmr(options: WasmHmrOptions): Plugin { + let server: ViteDevServer; + let diagnosticsWatcher: ReturnType | null = null; + + const diagnosticsFile = path.resolve(options.wasmPackagePath, '.wasm-build-status'); + + let debounceTimer: NodeJS.Timeout | null = null; + const DEBOUNCE_MS = 3000; + + return { + name: 'vite-plugin-wasm-hmr', + enforce: 'pre', + + configureServer(_server) { + server = _server; + + console.log('[wasm-hmr] Watching diagnostics:', diagnosticsFile); + + // Watch the bacon diagnostics file + diagnosticsWatcher = watch(diagnosticsFile, async (eventType) => { + if (eventType !== 'change') return; + + // Debounce: only process if no more changes in 100ms + if (debounceTimer) { + clearTimeout(debounceTimer); + } + + debounceTimer = setTimeout(() => { + processDiagnostics(); + }, DEBOUNCE_MS); + }); + + const processDiagnostics = () => { + console.log('[wasm-hmr] Processing diagnostics...'); + + // Read diagnostics file + if (!fs.existsSync(diagnosticsFile)) { + return; + } + + const diagnostics = fs.readFileSync(diagnosticsFile, 'utf-8').trim(); + + // Handle refreshing state + if (diagnostics === 'refreshing') { + console.log('[wasm-hmr] Build in progress...'); + server.ws.send({ + type: 'custom', + event: 'wasm-build-status', + data: { status: 'refreshing' }, + }); + // server.ws.send({ + // type: 'full-reload', + // path: '*', + + // }) + return; + } + + // Handle cancelled state (build was interrupted) + if (diagnostics === 'cancelled') { + console.log('[wasm-hmr] Build cancelled (likely restarted)'); + // Don't send anything to browser, just wait for next build + return; + } + + // Handle success state + if (diagnostics === 'success' || diagnostics === '') { + console.log('[wasm-hmr] Build succeeded, waiting for file system sync...'); + + // Small delay to ensure files are fully written and flushed to disk + // before triggering reload + setTimeout(() => { + // Copy WASM files to local path if configured + if (options.localCopyPath) { + const sourcePath = path.resolve(options.wasmPackagePath, options.watchPath); + const destPath = options.localCopyPath; + + console.log('[wasm-hmr] Copying WASM files from', sourcePath, 'to', destPath); + + try { + // Ensure destination directory exists + if (!fs.existsSync(destPath)) { + fs.mkdirSync(destPath, { recursive: true }); + } + + // Copy all files from source to destination + const files = fs.readdirSync(sourcePath); + for (const file of files) { + const srcFile = path.join(sourcePath, file); + const destFile = path.join(destPath, file); + + if (fs.statSync(srcFile).isFile()) { + fs.copyFileSync(srcFile, destFile); + console.log('[wasm-hmr] Copied', file); + } + } + } catch (error) { + console.error('[wasm-hmr] Error copying WASM files:', error); + } + } + + console.log('[wasm-hmr] Triggering WASM reload'); + server.ws.send({ + type: 'custom', + event: 'wasm-hard-reload', + data: { timestamp: Date.now() }, + }); + }, 500); + + return; + } + + // Handle error state + const hasErrors = diagnostics.split('\n').some(line => + line.trim().toLowerCase().includes('error') + ); + + if (hasErrors) { + console.log('[wasm-hmr] Build failed with errors'); + server.ws.send({ + type: 'error', + err: { + message: 'Rust compilation failed', + stack: formatBuildErrors(diagnostics), + plugin: 'vite-plugin-wasm-hmr', + }, + }); + } else { + // Has content but no errors - probably warnings, treat as success + console.log('[wasm-hmr] Build succeeded with warnings, triggering reload'); + // server.ws.send({ + // type: 'full-reload', + // path: '*', + // }); + } + }; + + server.httpServer?.on('close', () => { + if (debounceTimer) { + clearTimeout(debounceTimer); + } + diagnosticsWatcher?.close(); + }); + }, + + // handleHotUpdate({ file }) { + // // If a .rs file changes, we don't handle it directly + // // (bacon handles the rebuild), but we can notify the user + // if (file.endsWith('.rs')) { + // console.log('[wasm-hmr] Rust file changed:', file); + // console.log('[wasm-hmr] Waiting for bacon to rebuild...'); + // } + // return []; + // }, + + transformIndexHtml() { + return [ + { + tag: 'script', + injectTo: 'head', + attrs: { + type: 'module', + }, + children: ` +if (import.meta.hot) { + let buildStatusOverlay = null; + + import.meta.hot.on('wasm-build-status', (data) => { + if (data.status === 'refreshing') { + showBuildStatus('Rebuilding WASM...'); + } + }); + + import.meta.hot.on('wasm-hard-reload', async (data) => { + console.log('[wasm-hmr] WASM rebuild complete, invalidating module...'); + showBuildStatus('WASM rebuilt, hot reloading...'); + + // Invalidate the WASM module to force re-import with cache busting + // const wasmModulePath = '@gloo-ai/baml-schema-wasm-web/baml_schema_build'; + + // Use import.meta.hot.invalidate() to trigger HMR for this module + // import.meta.hot.invalidate(); + }); + + function showBuildStatus(message) { + if (!buildStatusOverlay) { + buildStatusOverlay = document.createElement('div'); + buildStatusOverlay.id = 'wasm-build-status'; + buildStatusOverlay.style.cssText = 'position:fixed;top:10px;right:10px;background:#1a1a1a;color:#4ade80;padding:8px 16px;border-radius:6px;font-family:monospace;font-size:12px;z-index:9999;box-shadow:0 2px 8px rgba(0,0,0,0.3);border:1px solid #4ade80;'; + document.body.appendChild(buildStatusOverlay); + } + buildStatusOverlay.textContent = message; + buildStatusOverlay.style.display = 'block'; + } +} + `, + }, + ]; + }, + }; +} + +/** + * Format build errors for Vite's error overlay + */ +function formatBuildErrors(diagnostics: string): string { + const lines = diagnostics.split('\n').filter(line => line.trim()); + + if (lines.length === 0) { + return 'Build failed with unknown errors'; + } + + const formatted: string[] = ['Rust Compilation Failed:\n']; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + // Highlight error lines + if (trimmed.toLowerCase().includes('error[e') || trimmed.toLowerCase().includes('error:')) { + formatted.push(`\n❌ ${trimmed}\n`); + } else if (trimmed.includes('-->')) { + formatted.push(` ${trimmed}`); + } else if (trimmed.startsWith('|')) { + formatted.push(` ${trimmed}`); + } else if (trimmed.toLowerCase().includes('help:') || trimmed.toLowerCase().includes('note:')) { + formatted.push(` 💡 ${trimmed}`); + } else { + formatted.push(` ${trimmed}`); + } + } + + return formatted.join('\n'); +} diff --git a/typescript/apps/playground/vite.config.ts b/typescript/apps/playground/vite.config.ts index 6d6e25884b..5f9c28531f 100644 --- a/typescript/apps/playground/vite.config.ts +++ b/typescript/apps/playground/vite.config.ts @@ -5,11 +5,16 @@ import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; import wasm from 'vite-plugin-wasm'; import { viteStaticCopy } from 'vite-plugin-static-copy' +import { wasmHmr } from './plugins/vite-plugin-wasm-hmr'; const isWatchMode = process.argv.includes('--watch'); const srcPath = normalizePath(path.resolve(__dirname, './dist/')); const destPath = normalizePath(path.resolve(__dirname, '../vscode-ext/dist/playground')); +// Path to WASM source and local copy +const wasmSourcePath = path.resolve(__dirname, '../../../engine/baml-schema-wasm/web/dist'); +const wasmLocalPath = path.resolve(__dirname, './baml-schema-wasm-web/dist'); + // https://vitejs.dev/config/ export default defineConfig({ plugins: [ @@ -19,11 +24,20 @@ export default defineConfig({ }, }), wasm(), + wasmHmr({ + wasmPackagePath: path.resolve(__dirname, '../../../engine/baml-schema-wasm/web'), + watchPath: 'dist', + localCopyPath: wasmLocalPath, + }), viteStaticCopy({ targets: [ { src: srcPath, dest: destPath + }, + { + src: normalizePath(wasmSourcePath) + '/*', + dest: normalizePath(wasmLocalPath) } ] }) @@ -39,6 +53,9 @@ export default defineConfig({ }, headers: { 'Access-Control-Allow-Origin': '*', + // Prevent caching of WASM files during development + // TODO: idk if this actually does anything. + 'Cache-Control': 'no-store', }, hmr: { // This is needed for HMR to work in VSCode webviews @@ -48,20 +65,15 @@ export default defineConfig({ watch: { usePolling: true, interval: 100, + ignored: ['../../../engine/baml-schema-wasm/web/dist/**/*.wasm'], }, }, resolve: { alias: { '@': path.resolve(__dirname, './src'), '~': path.resolve(__dirname, './src'), - '@gloo-ai/baml-schema-wasm-web': path.resolve( - __dirname, - '../../../engine/baml-schema-wasm/web/dist', - ), - baml_wasm_web: path.resolve( - __dirname, - '../../../engine/baml-schema-wasm/web/dist', - ), + '@gloo-ai/baml-schema-wasm-web': wasmLocalPath, + baml_wasm_web: wasmLocalPath, }, }, mode: isWatchMode ? 'development' : 'production', @@ -83,5 +95,7 @@ export default defineConfig({ esbuildOptions: { target: 'esnext', }, + + // exclude: ['@gloo-ai/baml-schema-wasm-web'], }, }); diff --git a/typescript/packages/playground-common/src/shared/baml-project-panel/atoms.ts b/typescript/packages/playground-common/src/shared/baml-project-panel/atoms.ts index fe33ce0947..5a306a43b9 100644 --- a/typescript/packages/playground-common/src/shared/baml-project-panel/atoms.ts +++ b/typescript/packages/playground-common/src/shared/baml-project-panel/atoms.ts @@ -1,5 +1,5 @@ import { atom, getDefaultStore, useAtomValue, useSetAtom } from 'jotai'; -import { atomFamily, atomWithStorage } from 'jotai/utils'; +import { atomFamily, atomWithStorage, atomWithReset, RESET } from 'jotai/utils'; import { useEffect } from 'react'; import type { @@ -118,20 +118,40 @@ export const betaFeatureEnabledAtom = atom((get) => { }); -let wasmAtomAsync = atom(async () => { - const wasm = await import('@gloo-ai/baml-schema-wasm-web/baml_schema_build'); +// Trigger atom to force WASM reload - increment this to reload WASM +const wasmReloadTriggerAtom = atom(0); + +let wasmAtomAsync = atom(async (get) => { + // Subscribe to the trigger to force reload when it changes + const trigger = get(wasmReloadTriggerAtom); + console.log("sam Loading WASM module, trigger:", trigger) + + // Add cache busting to the import with timestamp + const wasm = await import(`@gloo-ai/baml-schema-wasm-web/baml_schema_build`); // Enable WASM logging for debugging wasm.init_js_callback_bridge(vscode.loadAwsCreds, vscode.loadGcpCreds); return wasm; -}, - - async (_get, set, newValue: null) => { - set(wasmAtomAsync, null) - } -); +}); export const wasmAtom = unwrap(wasmAtomAsync); +const store = getDefaultStore(); + +const hot = (import.meta as any).hot; +if (hot) { + console.log("sam HMR import.meta.hot", hot); + + // Listen for custom WASM reload events from the plugin + hot.on('wasm-hard-reload', (data: unknown) => { + console.log("sam HMR received wasm-hard-reload event, triggering reload...", data); + + // Increment the trigger to force WASM atom to re-evaluate + // const currentValue = store.get(wasmReloadTriggerAtom); + // store.set(wasmReloadTriggerAtom, currentValue + 1); + }); +} + + export const useWaitForWasm = () => { const wasm = useAtomValue(wasmAtom); return wasm !== undefined; diff --git a/typescript/packages/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/test-panel/components/EnhancedErrorRenderer.tsx b/typescript/packages/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/test-panel/components/EnhancedErrorRenderer.tsx index 06517bb402..ae54ca3f82 100644 --- a/typescript/packages/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/test-panel/components/EnhancedErrorRenderer.tsx +++ b/typescript/packages/playground-common/src/shared/baml-project-panel/playground-panel/prompt-preview/test-panel/components/EnhancedErrorRenderer.tsx @@ -46,7 +46,6 @@ const CopyErrorButton: React.FC<{ errorMessage: string }> = ({ errorMessage }) = setTimeout(() => setCopyStatus('idle'), 2000); } }; - const getButtonStyle = () => { if (copyStatus === 'success') { return { @@ -99,6 +98,7 @@ const ErrorDetails: React.FC<{ errorMessage: string }> = ({ errorMessage }) => { variant="outline" size="xs" className="inline-flex items-center gap-1 text-xs px-2 py-0 h-7 transition-colors duration-150 border-[var(--vscode-panel-border)] text-[var(--vscode-charts-red)] bg-[var(--vscode-editor-background)] cursor-pointer hover:opacity-80" + style={{ color: '#dc2626', background: 'var(--vscode-editor-background)',