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;
|
2023-09-22 20:41:34 +00:00
|
|
|
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);
|
|
|
|
|
|
2023-09-22 20:41:34 +00:00
|
|
|
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));
|
2023-09-22 20:41:34 +00:00
|
|
|
})),
|
|
|
|
|
..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(())
|
|
|
|
|
}
|