use crate::exec::{fixtures, runtime}; use crate::reporting::{CompilerError, InterpreterError, Problem}; use deno_core::{v8, Extension, OpState}; use elm_project_utils::{setup_generator_project, ElmPostProcessor, ElmResult}; use os_pipe::dup_stderr; use serde::{Deserialize, Serialize}; use sqlx::sqlite::SqlitePool; use sqlx::Row; use std::cell::RefCell; use std::convert::TryFrom; use std::fs; use std::io; use std::io::Write; use std::path::PathBuf; use std::process::{Command, Stdio}; use std::sync::Arc; use tokio; use tracing::info_span; pub(crate) fn run( debug: bool, verbosity: u64, elm_project_dir: PathBuf, source_checksum: u64, entrypoint: elmi::Global, output: Option, ) -> Result<(), Problem> { // TODO require that output type is a directory // step 6 create our private project let packages = ["ThinkAlexandria/css-in-elm", "elm/json"]; // I added a couple of random bytes to the directory name to reduce the risk of // collisions with other programs that also use elm-stuff for their scratch space let our_temp_dir = elm_project_dir.join("elm-stuff").join("starmelon-ce7993"); let generator_dir = setup_generator_project(verbosity, elm_project_dir.clone(), our_temp_dir, &packages)?; // step 7 create an Elm fixture file to run our function let span = info_span!("create Elm fixture files"); let timing_guard = span.enter(); let (gen_module_name, source) = fixtures::css_in_elm::generate(source_checksum, &entrypoint); let mut source_filename = generator_dir.join("src").join(&gen_module_name); source_filename.set_extension("elm"); let span = info_span!("file writes"); let file_write_timing_guard = span.enter(); fs::File::create(&source_filename) .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, source_filename.clone()))? .write_all(source.as_bytes()) .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, source_filename.clone()))?; drop(file_write_timing_guard); drop(timing_guard); // step 8 compile the fixture let mut intermediate_file = generator_dir.join("obj").join(&gen_module_name); intermediate_file.set_extension("js"); let mut command = Command::new("elm"); command .arg("make") .arg("--output") .arg(&intermediate_file) .current_dir(&generator_dir) .stdin(Stdio::null()) .stderr(Stdio::piped()); if verbosity < 1 { command.stdout(Stdio::piped()); } else { let pipe = dup_stderr() .map_err(|io_err| CompilerError::ReadInputFailed(io_err, "stdout".into()))?; command.stdout(pipe); } if debug { command.arg("--debug"); } command.arg(&source_filename); let span = info_span!("elm make fixture file"); let timing_guard = span.enter(); match command.output() { Ok(output) => { if !output.status.success() { eprintln!("{}", String::from_utf8_lossy(&output.stderr)); return Err(CompilerError::FailedBuildingFixture.into()); } } Err(err) => { eprintln!("{:?}", err); return Err(CompilerError::FailedBuildingFixture.into()); } } drop(timing_guard); let data = fs::read_to_string(&intermediate_file) .map_err(|io_err| CompilerError::ReadInputFailed(io_err, intermediate_file.clone()))?; // TODO figure out how to replace multiple substrings in a single pass. One neat trick // might be to allocate enough space in our starting buffer to write the new code by // having a really large replace block. For example if we are replacing a string // `"REPLACE_ME" + "abc" * 2000` we would have over 6k bytes to write out the new code. We // will have to do some extra book keeping to make sure the buffer space is big enough // for the replacement code. let span = info_span!("munge fixture javascript"); let timing_guard = span.enter(); let mut munger = ElmPostProcessor::new(); let mut buffer = String::with_capacity(data.len()); munger.run(&data, &mut buffer); // Mutate the buffer in place for the final fixup let pattern = ";}(this));"; match buffer.get((buffer.len() - pattern.len())..) { Some(end) if end == pattern => { buffer.truncate(buffer.len() - pattern.len()); buffer.push_str(";}(globalThis));"); } _ => (), } // I think that when I set this script to be the main module, I am skipping the // deno/runtime/js/99_main.js script that sets up a bunch of global variables. If I // manually add the timer related code below then setTimeout works again. // NB. there are 706 lines of setup code that add a bunch of apis to the global window // scope. Figure out if I need to include all of them. For example, starmelon does not need // to perform http calls right now, but I eventually want to. buffer.push_str( r#" const { setTimeout } = globalThis.__bootstrap.timers; Deno.core.setMacrotaskCallback(globalThis.__bootstrap.timers.handleTimerMacrotask); globalThis.setTimeout = setTimeout; "#, ); buffer.push_str(&format!("var worker = Elm.{}.init();\n", &gen_module_name)); // add a shortcut for invoking the function so I don't have to traverse so many object // lookups using the rust v8 API. buffer.push_str( r#" globalThis.runOnInput = function(route) {}; if (worker.ports.onFilesOutput) { worker.ports.onFilesOutput.subscribe(function(result){ Deno.core.opSync('op_starmelon_elm_css_files_output', result) }); }"#, ); drop(timing_guard); let mut buffer_file = generator_dir.join("bin").join(&gen_module_name); buffer_file.set_extension("js"); let span = info_span!("file writes"); let timing_guard = span.enter(); std::fs::write(&buffer_file, buffer) .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, buffer_file.clone()))?; drop(timing_guard); let desired_route = entrypoint.0.module.clone().to_string(); let foo = move |mut scope: deno_core::v8::HandleScope| -> Result<(), InterpreterError> { Ok(()) // I don't think we have to provide any input to kick the process off //let scope = &mut scope; //let ctx = scope.get_current_context(); //let global = ctx.global(scope); //let entrypoint = { // let x = // v8::String::new(scope, "runOnInput").ok_or(InterpreterError::AllocationFailed)?; // v8::Local::new(scope, x).into() //}; //let v8_value = global // .get(scope, entrypoint) // .ok_or(InterpreterError::ReferenceError)?; //// step 12 invoke the function //let function = v8::Local::::try_from(v8_value)?; //let this = v8::undefined(scope).into(); //let span = info_span!("dispatch v8 call"); //let timing_guard = span.enter(); //let arg1 = { // let x = // v8::String::new(scope, &desired_route).ok_or(InterpreterError::AllocationFailed)?; // v8::Local::new(scope, x).into() //}; //function.call(scope, this, &[arg1]); //drop(timing_guard); //Ok(()) }; // Create a tokio runtime before registering ops so we can block on futures inside sync ops let span = info_span!("create tokio runtime"); let timing_guard = span.enter(); let sys = tokio::runtime::Builder::new_current_thread() // The default number of additional threads for running blocking FnOnce is 512. .max_blocking_threads(1) .enable_all() .build() .unwrap(); drop(timing_guard); let span = info_span!("register private api"); let timing_guard = span.enter(); // Step 10 setup our custom ops for the javascript runtime let mailbox: Arc>>> = Arc::new(RefCell::new(None)); let mailbox_clone = Arc::clone(&mailbox); let mut extensions = vec![Extension::builder() .ops(vec![op_starmelon_elm_css_files_output::decl()]) .state(move |state| { state.put(Arc::clone(&mailbox_clone)); Ok(()) }) .build()]; drop(timing_guard); // step 11 create a v8 isolate. let span = info_span!("create v8 isolate"); let timing_guard = span.enter(); let (worker, main_module) = runtime::setup_worker(extensions, &buffer_file.to_string_lossy()) .map_err(|err| InterpreterError::EventLoop(err))?; drop(timing_guard); let span = info_span!("eval javascript"); let timing_guard = span.enter(); sys.block_on(async move { runtime::xyz(worker, main_module, foo).await })?; drop(timing_guard); // step 13 receive the callback // If I understood which combination of run_event_loop was required to execute // the javascript to completion then we should have something in our mailbox by // now. Another way to do this would be with an Arc. This will panic if the // mailbox is currently borrowed match mailbox.replace(None) { Some(files) => { let base = match output { None => { std::env::current_dir().unwrap() } Some(base) => base, }; for FileDefinition { filename, content, success } in files.iter() { if *success { let outfile = base.join(&filename); let mut f = fs::File::create(&outfile) .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, outfile.clone()))?; f.write_all(content.as_bytes()) .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, outfile))?; } else { eprintln!("{} failed\n{}", filename, content) } } }, None => eprintln!("nothing in the mailbox"), } Ok(()) } type OutputMailbox = Arc>>>; #[derive(Deserialize, Debug)] struct FileDefinition { filename: String, content: String, success: bool, } #[deno_core::op] fn op_starmelon_elm_css_files_output( state: &mut OpState, msg: Vec, ) -> Result<(), deno_core::error::AnyError> { let mailbox_clone = state.borrow::(); if let Ok(mut mailbox) = mailbox_clone.try_borrow_mut() { mailbox.replace(msg); } Ok(()) }