starmelon/src/exec/css_in_elm.rs

254 lines
9.3 KiB
Rust
Raw Normal View History

2022-04-02 23:54:12 +01:00
use crate::exec::{fixtures, runtime};
use crate::reporting::{CompilerError, InterpreterError, Problem};
2022-04-03 00:15:07 +01:00
use deno_core::{Extension, OpState};
use elm_project_utils::{setup_generator_project, ElmPostProcessor};
2022-04-02 23:54:12 +01:00
use os_pipe::dup_stderr;
2022-04-03 00:15:07 +01:00
use serde::Deserialize;
use std::borrow::Cow;
2022-04-02 23:54:12 +01:00
use std::cell::RefCell;
use std::fs;
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<PathBuf>,
) -> 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#"
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);
2022-04-03 00:15:07 +01:00
let _desired_route = entrypoint.0.module.clone().to_string();
let foo = move |_scope: deno_core::v8::HandleScope| -> Result<(), InterpreterError> { Ok(()) };
2022-04-02 23:54:12 +01:00
// 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<RefCell<Option<Vec<FileDefinition>>>> = Arc::new(RefCell::new(None));
let mailbox_clone = Arc::clone(&mailbox);
let extensions = vec![Extension {
ops: Cow::Owned(vec![op_starmelon_elm_css_files_output::decl()]),
op_state_fn: Some(Box::new(move |state| {
2022-04-02 23:54:12 +01:00
state.put(Arc::clone(&mailbox_clone));
})),
..Default::default()
}];
2022-04-02 23:54:12 +01:00
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 {
2022-04-03 00:15:07 +01:00
None => std::env::current_dir().unwrap(),
2022-04-02 23:54:12 +01:00
Some(base) => base,
};
2022-04-03 00:15:07 +01:00
for FileDefinition {
filename,
content,
success,
} in files.iter()
{
2022-04-02 23:54:12 +01:00
if *success {
let outfile = base.join(&filename);
2022-04-03 00:15:07 +01:00
let mut f = fs::File::create(&outfile).map_err(|io_err| {
CompilerError::WriteOutputFailed(io_err, outfile.clone())
})?;
2022-04-02 23:54:12 +01:00
f.write_all(content.as_bytes())
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, outfile))?;
} else {
eprintln!("{} failed\n{}", filename, content)
}
}
2022-04-03 00:15:07 +01:00
}
2022-04-02 23:54:12 +01:00
None => eprintln!("nothing in the mailbox"),
}
Ok(())
}
type OutputMailbox = Arc<RefCell<Option<Vec<FileDefinition>>>>;
#[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<FileDefinition>,
) -> Result<(), deno_core::error::AnyError> {
let mailbox_clone = state.borrow::<OutputMailbox>();
if let Ok(mut mailbox) = mailbox_clone.try_borrow_mut() {
mailbox.replace(msg);
}
Ok(())
}