use std::path::PathBuf;
use std::env;
use boa_engine::{Context, Source};
use boa_engine::property::Attribute;
use boa_engine::object::ObjectInitializer;
use boa_runtime::Console;
use crate::bundler::Bundler;
use crate::error::Result;
use crate::es202x::ES202XFeatures;
pub async fn run_file(file: PathBuf, _args: Vec<String>) -> Result<()> {
let current_dir = env::current_dir()?;
let mut bundler = Bundler::new(current_dir);
// Bundle the entry point with all dependencies (CJS format for require/module.exports)
let bundle: String = bundler.bundle_to_string(&file, "cjs", false).await?;
// Create Boa context
let mut context = Context::default();
// Register console for console.log, etc.
let console = Console::init(&mut context);
context
.register_global_property(Console::NAME, console, Attribute::all())
.map_err(|e| crate::error::DumplingError::Build(format!("Failed to register console: {}", e)))?;
// Register process.env for Node.js compatibility
let process_env = create_process_env(&mut context)?;
context
.register_global_property("process", process_env, Attribute::all())
.map_err(|e| crate::error::DumplingError::Build(format!("Failed to register process: {}", e)))?;
// Polyfill Object.hasOwn (ES2022) if not present
let has_own_polyfill = b"if(typeof Object.hasOwn!=='function'){Object.hasOwn=function(o,p){return Object.prototype.hasOwnProperty.call(o,p);};}";
context
.eval(Source::from_bytes(has_own_polyfill))
.map_err(|e| crate::error::DumplingError::Build(format!("Polyfill error: {}", e)))?;
// Initialize ES202X features
ES202XFeatures::init(&mut context)
.map_err(|e| crate::error::DumplingError::Build(format!("Failed to initialize ES202X features: {}", e)))?;
// Execute the bundled JavaScript with enhanced error handling
match context.eval(Source::from_bytes(bundle.as_bytes())) {
Ok(_) => Ok(()),
Err(e) => {
// Create detailed error with source map information if available
let mut error_context = crate::error::ErrorContext::new()
.with_file(file.clone());
// Try to extract line and column information from the error
let error_string = format!("{}", e);
if let Some(line_col) = extract_line_column_from_error(&error_string) {
error_context = error_context
.with_line(line_col.0)
.with_column(line_col.1);
// Extract relevant source code line
if let Ok(source_content) = std::fs::read_to_string(&file) {
let lines: Vec<&str> = source_content.lines().collect();
if let Some(source_line) = lines.get((line_col.0 as usize).saturating_sub(1)) {
error_context = error_context.with_source_code(format!("{}\n{}",
source_line,
" ".repeat(line_col.1 as usize) + "^"
));
}
}
}
Err(crate::error::DumplingError::Runtime(e).with_context(error_context).into())
}
}
}
fn create_process_env(context: &mut Context) -> std::result::Result<boa_engine::JsValue, crate::error::DumplingError> {
// Build env object with real environment variables
let mut env_init = ObjectInitializer::new(context);
for (key, value) in env::vars() {
env_init.property(key, value, Attribute::all());
}
let env_js = env_init.build();
let cwd = env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.display()
.to_string();
let platform = std::env::consts::OS;
let arch = std::env::consts::ARCH;
let process = ObjectInitializer::new(context)
.property("env", env_js, Attribute::all())
.property("cwd", cwd, Attribute::all())
.property("platform", platform, Attribute::all())
.property("arch", arch, Attribute::all())
.build();
Ok(process.into())
}
/// Extract line and column information from error messages
fn extract_line_column_from_error(error_str: &str) -> Option<(u32, u32)> {
// Try to match patterns like "at line 123:45" or "at line 123, column 45"
let line_col_regex = regex::Regex::new(r"at line (\d+)(?::(\d+))|at line (\d+), column (\d+)").ok()?;
if let Some(caps) = line_col_regex.captures(error_str) {
let line = caps.get(1).or(caps.get(3))?.as_str().parse().ok()?;
let column = caps.get(2).or(caps.get(4))
.map(|m| m.as_str().parse().unwrap_or(0))
.unwrap_or(0);
return Some((line, column));
}
None
}
#[derive(Clone)]
enum HmrUpdate {
Reload,
Css { path: String, content: String },
}
pub async fn start_dev_server(host: String, port: u16) -> Result<()> {
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::broadcast;
use std::sync::mpsc;
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
let addr = format!("{}:{}", host, port);
let listener = TcpListener::bind(&addr).await
.map_err(|e| crate::error::DumplingError::Build(format!("Failed to bind to {}: {}", addr, e)))?;
let (reload_tx, _) = broadcast::channel::<HmrUpdate>(16);
let root = env::current_dir()?;
// File watcher - bridge sync notify to async broadcast, send changed paths
let (fs_tx, fs_rx) = mpsc::channel::<Vec<std::path::PathBuf>>();
let watcher_root = root.clone();
if let Ok(mut watcher) = RecommendedWatcher::new(
move |res: std::result::Result<notify::Event, notify::Error>| {
if let Ok(ev) = res {
if !ev.paths.is_empty() {
let _ = fs_tx.send(ev.paths);
}
}
},
Config::default(),
) {
if watcher.watch(&watcher_root, RecursiveMode::Recursive).is_ok() {
std::mem::forget(watcher);
let watcher_tx = reload_tx.clone();
std::thread::spawn(move || {
while let Ok(paths) = fs_rx.recv() {
let has_non_css = paths.iter().any(|p| {
p.extension().map(|e| e.to_string_lossy()) != Some(std::borrow::Cow::Borrowed("css"))
});
if has_non_css {
let _ = watcher_tx.send(HmrUpdate::Reload);
} else {
for path in paths {
if path.extension().map(|e| e.to_string_lossy()) == Some(std::borrow::Cow::Borrowed("css"))
&& path.is_file()
{
if let Ok(content) = std::fs::read_to_string(&path) {
let hmr_path = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("style.css")
.to_string();
let _ = watcher_tx.send(HmrUpdate::Css {
path: hmr_path,
content,
});
}
}
}
}
}
});
}
}
println!("Starting dev server on http://{}", addr);
println!("HMR enabled - CSS hot-reload, JS triggers full reload");
println!("Press Ctrl+C to stop");
loop {
let (mut socket, _) = listener.accept().await
.map_err(|e| crate::error::DumplingError::Build(format!("Accept error: {}", e)))?;
let root = root.clone();
let request = {
let mut buf = [0u8; 8192];
if let Ok(n) = socket.read(&mut buf).await {
String::from_utf8_lossy(&buf[..n]).to_string()
} else {
continue;
}
};
let path = parse_request_path(&request);
let is_hmr = path == "/__hmr";
if is_hmr {
// HMR polling - returns JSON: {"type":"reload"} or {"type":"css","path":"...","content":"..."}
let mut sub = reload_tx.subscribe();
let update = sub.recv().await;
let body = match update {
Ok(HmrUpdate::Reload) => serde_json::json!({"type": "reload"}).to_string(),
Ok(HmrUpdate::Css { path: p, content }) => {
serde_json::json!({"type": "css", "path": p, "content": content}).to_string()
}
Err(_) => serde_json::json!({"type": "reload"}).to_string(),
};
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let _ = socket.write_all(response.as_bytes()).await;
} else {
// Regular HTTP
let file_path = if path == "/" || path.is_empty() {
root.join("index.html")
} else {
root.join(path.trim_start_matches('/'))
};
let (status, body, content_type) = if file_path.exists() && file_path.is_file() {
match tokio::fs::read_to_string(&file_path).await {
Ok(content) => {
let ct = content_type_for_path(&file_path);
let body = if ct == "text/html" {
inject_hmr_script(&content, &addr)
} else {
content
};
("200 OK", body, ct)
}
Err(_) => ("500 Internal Server Error", "Internal Server Error".to_string(), "text/plain"),
}
} else {
("404 Not Found", "Not Found".to_string(), "text/plain")
};
let response = format!(
"HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
status,
content_type,
body.len(),
body
);
let _ = socket.write_all(response.as_bytes()).await;
}
}
}
fn inject_hmr_script(html: &str, addr: &str) -> String {
let error_overlay = r#"<div id="dumpling-error-overlay" style="display:none;position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.85);color:#ff6b6b;font-family:monospace;font-size:14px;padding:20px;overflow:auto;box-sizing:border-box"><div style="max-width:800px;margin:0 auto"><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px"><strong style="font-size:18px">Runtime Error</strong><button id="dumpling-error-dismiss" style="background:#333;color:#fff;border:none;padding:8px 16px;cursor:pointer;border-radius:4px">Dismiss</button></div><pre id="dumpling-error-content" style="white-space:pre-wrap;word-break:break-all;margin:0;color:#eee"></pre></div></div>"#;
let error_script = r#"
(function(){
function showError(msg,stack){
var o=document.getElementById('dumpling-error-overlay');
var c=document.getElementById('dumpling-error-content');
if(o&&c){c.textContent=msg+(stack?'\n\n'+stack:'');o.style.display='block';}
}
window.onerror=function(m,u,l,c,e){showError(m+(e&&e.stack?'\n'+e.stack:''),'');return false;};
window.onunhandledrejection=function(e){showError('Unhandled rejection: '+(e.reason&&(e.reason.message||String(e.reason))),(e.reason&&e.reason.stack)||'');};
var b=document.getElementById('dumpling-error-dismiss');
if(b)b.onclick=function(){var o=document.getElementById('dumpling-error-overlay');if(o)o.style.display='none';};
})();
"#;
let hmr_script = format!(
r#"{}<script>{}</script><script>(function(){{function poll(){{fetch('http://{}/__hmr').then(function(r){{if(r.ok)return r.json()}}).then(function(d){{if(!d)return;if(d.type==='reload')location.reload();if(d.type==='css'){{var s=document.querySelector('[data-dumpling-hmr="'+d.path+'"]');if(s)s.textContent=d.content;else{{s=document.createElement('style');s.setAttribute('data-dumpling-hmr',d.path);s.textContent=d.content;(document.head||document.documentElement).appendChild(s)}}}}}}).catch(function(){{}});setTimeout(poll,1000)}}poll()}}</script></body>"#,
error_overlay,
error_script,
addr
);
if html.contains("</body>") {
html.replace("</body>", &hmr_script)
} else {
format!("{}{}", html, hmr_script)
}
}
fn parse_request_path(request: &str) -> &str {
request
.lines()
.next()
.and_then(|line| line.split_whitespace().nth(1))
.unwrap_or("/")
}
fn content_type_for_path(path: &PathBuf) -> &'static str {
path.extension()
.and_then(|e| e.to_str())
.map(|ext| match ext {
"html" => "text/html",
"css" => "text/css",
"js" => "application/javascript",
"json" => "application/json",
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"gif" => "image/gif",
"svg" => "image/svg+xml",
"ico" => "image/x-icon",
"woff" => "font/woff",
"woff2" => "font/woff2",
_ => "application/octet-stream",
})
.unwrap_or("application/octet-stream")
}