feat: port css-in-elm to starmelon
This commit is contained in:
parent
80f7585153
commit
376a14480b
5 changed files with 465 additions and 0 deletions
290
src/exec/css_in_elm.rs
Normal file
290
src/exec/css_in_elm.rs
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
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<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#"
|
||||
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::<v8::Function>::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<RefCell<Option<Vec<FileDefinition>>>> = 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<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(())
|
||||
}
|
||||
69
src/exec/fixtures/css_in_elm.rs
Normal file
69
src/exec/fixtures/css_in_elm.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
use crate::exec::astrid_pages::OutputType;
|
||||
use elm_quote::Tokens;
|
||||
use genco::tokens::quoted;
|
||||
|
||||
pub(crate) fn generate(source_checksum: u64, entrypoint: &elmi::Global) -> (String, String) {
|
||||
let tokens: Tokens = genco::quote! {
|
||||
import Css
|
||||
import Css.File
|
||||
import Platform
|
||||
import Stylesheets
|
||||
|
||||
-- START CUSTOMIZED PART
|
||||
import #(&entrypoint.0.module)
|
||||
|
||||
entrypoint = #(&entrypoint.0.module).#(&entrypoint.1)
|
||||
-- END CUSTOMIZED PART
|
||||
|
||||
-- MAIN
|
||||
|
||||
main: Program () Model Never
|
||||
main =
|
||||
Platform.worker
|
||||
{ init = init
|
||||
, update = update
|
||||
, subscriptions = subscriptions
|
||||
}
|
||||
|
||||
-- PORTS
|
||||
|
||||
port onFilesOutput : Css.File.CssFileStructure -> Cmd msg
|
||||
|
||||
-- MODEL
|
||||
|
||||
type alias Model = ()
|
||||
|
||||
init : () -> (Model, Cmd Never)
|
||||
init flags =
|
||||
( (), onFilesOutput structure )
|
||||
|
||||
structure : Css.File.CssFileStructure
|
||||
structure =
|
||||
Css.File.toFileStructure <|
|
||||
List.map
|
||||
(#("\\")(fileName, stylesheets) ->
|
||||
(fileName, Css.File.compile stylesheets)
|
||||
)
|
||||
entrypoint
|
||||
|
||||
-- UPDATE
|
||||
|
||||
update : Never -> Model -> ((), Cmd Never)
|
||||
update _ model =
|
||||
( model, Cmd.none )
|
||||
|
||||
-- SUBSCRIPTIONS
|
||||
|
||||
subscriptions _ = Sub.none
|
||||
};
|
||||
|
||||
let module_name = format!("Main{:020}", source_checksum);
|
||||
|
||||
let source = format!(
|
||||
"port module {} exposing (main)\n\n{}",
|
||||
module_name,
|
||||
tokens.to_file_string().unwrap(),
|
||||
);
|
||||
|
||||
(module_name, source)
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod astrid_pages;
|
||||
pub mod css_in_elm;
|
||||
pub mod scripting;
|
||||
pub mod sqlite;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use std::path::PathBuf;
|
|||
use tracing::info_span;
|
||||
|
||||
mod astrid_pages;
|
||||
mod css_in_elm;
|
||||
mod fixtures;
|
||||
mod scripting;
|
||||
|
||||
|
|
@ -58,6 +59,8 @@ pub(crate) fn exec(
|
|||
|
||||
if is_astrid_pages_route(tipe) {
|
||||
ExecutionMode::AstridPagesRoute
|
||||
} else if is_css_in_elm_stylesheet(tipe) {
|
||||
ExecutionMode::CssInElm
|
||||
} else {
|
||||
let (input_type, output_type) = scripting::resolve_function_type(tipe)?;
|
||||
ExecutionMode::Scripting(input_type, output_type)
|
||||
|
|
@ -98,11 +101,20 @@ pub(crate) fn exec(
|
|||
entrypoint,
|
||||
output,
|
||||
),
|
||||
ExecutionMode::CssInElm => css_in_elm::run(
|
||||
debug,
|
||||
verbosity,
|
||||
elm_project_dir,
|
||||
source_checksum,
|
||||
entrypoint,
|
||||
output,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
enum ExecutionMode {
|
||||
AstridPagesRoute,
|
||||
CssInElm,
|
||||
Scripting(Option<scripting::InputType>, scripting::OutputType),
|
||||
}
|
||||
|
||||
|
|
@ -115,6 +127,40 @@ fn is_astrid_pages_route(tipe: &elmi::Type) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
fn is_css_in_elm_stylesheet(tipe: &elmi::Type) -> bool {
|
||||
match tipe {
|
||||
elmi::Type::TType(module_name, name, args) => {
|
||||
if module_name == "elm/core/List" && name == "List" && args.len() == 1 {
|
||||
match &args[0] {
|
||||
elmi::Type::TTuple(a, b, None) => {
|
||||
match &**a {
|
||||
elmi::Type::TType(module_name, name, _)
|
||||
if module_name == "elm/core/String" && name == "String" => (),
|
||||
_ => return false,
|
||||
}
|
||||
match &**b {
|
||||
elmi::Type::TType(module_name, name, args)
|
||||
if module_name == "elm/core/List" && name == "List" && args.len() == 1 =>
|
||||
{
|
||||
match &args[0] {
|
||||
elmi::Type::TAlias(module_name, name, _, _)
|
||||
if module_name == "ThinkAlexandria/css-in-elm/Css" && name == "Stylesheet" => return true,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
mod runtime {
|
||||
use crate::reporting::InterpreterError;
|
||||
use deno_core::error::{type_error, AnyError};
|
||||
|
|
|
|||
59
src/main.rs
59
src/main.rs
|
|
@ -8,6 +8,7 @@ use std::time::Instant;
|
|||
use structopt::StructOpt;
|
||||
use tracing::info_span;
|
||||
|
||||
mod derive;
|
||||
mod elm;
|
||||
mod exec;
|
||||
mod reporting;
|
||||
|
|
@ -70,6 +71,45 @@ fn main() {
|
|||
} => {
|
||||
transpile::transpile(file, debug, function, verbosity).unwrap();
|
||||
}
|
||||
Arguments::Derive(DeriveMacros::LiveTable {
|
||||
file,
|
||||
debug,
|
||||
verbosity,
|
||||
output,
|
||||
timings,
|
||||
}) => {
|
||||
let start = Instant::now();
|
||||
let span = info_span!("derive livetable editor");
|
||||
let timing_guard = span.enter();
|
||||
let result =
|
||||
derive::derive_livetable(file, debug, output, verbosity /*, sqlite */);
|
||||
drop(timing_guard);
|
||||
if let Err(problem) = result {
|
||||
let span = info_span!("pretty print problem");
|
||||
let timing_guard = span.enter();
|
||||
eprintln!("{}", pretty(80, problem.to_doc()));
|
||||
drop(timing_guard);
|
||||
}
|
||||
{
|
||||
let stdout = io::stdout();
|
||||
let mut handle = stdout.lock();
|
||||
handle.flush().unwrap();
|
||||
}
|
||||
|
||||
if timings {
|
||||
let mut report = timing_report.dump().unwrap();
|
||||
report.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap());
|
||||
|
||||
for (step, duration) in report.iter() {
|
||||
eprintln!("[{:>10.3?}] {}", duration, step);
|
||||
}
|
||||
}
|
||||
eprintln!(
|
||||
"\t\x1b[1;92mFinished\x1b[0m [{}] in {:?}",
|
||||
"unoptimized",
|
||||
Instant::now() - start
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,6 +171,25 @@ enum Arguments {
|
|||
#[structopt(long)]
|
||||
debug: bool,
|
||||
},
|
||||
#[structopt(name = "derive")]
|
||||
Derive(DeriveMacros),
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub enum DeriveMacros {
|
||||
#[structopt(name = "livetable")]
|
||||
LiveTable {
|
||||
#[structopt(parse(from_os_str))]
|
||||
file: PathBuf,
|
||||
#[structopt(short = "v", parse(from_occurrences))]
|
||||
verbosity: u64,
|
||||
#[structopt(long)]
|
||||
debug: bool,
|
||||
#[structopt(long)]
|
||||
output: Option<PathBuf>,
|
||||
#[structopt(long)]
|
||||
timings: bool,
|
||||
},
|
||||
}
|
||||
|
||||
pub mod generated {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue