Skip to content

Commit

Permalink
Export config dialog (#185)
Browse files Browse the repository at this point in the history
* compiler wasm export API; start working on export dialog

* dialogs done. needs cleanup

* clean up

* clean up comments

* upgrade rust to 1.76
  • Loading branch information
Pistonight authored Feb 10, 2024
1 parent 46710c8 commit 5ccb484
Show file tree
Hide file tree
Showing 31 changed files with 915 additions and 221 deletions.
2 changes: 1 addition & 1 deletion compiler-core/src/comp/comp_section.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl<'p> Compiler<'p> {
pub async fn compile_section(
&self,
value: RouteBlobRef<'p>,
route: &Vec<CompSection>,
route: &[CompSection],
) -> Option<CompSection> {
let result = match value.try_as_single_key_object() {
RouteBlobSingleKeyObjectResult::Ok(key, value) => Ok((key, value)),
Expand Down
98 changes: 88 additions & 10 deletions compiler-core/src/expo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
//! The output is a [`ExpoContext`].
use serde_json::Value;

use crate::comp::CompDoc;
use crate::exec::ExecContext;
use crate::macros::derive_wasm;

Expand Down Expand Up @@ -40,14 +41,16 @@ pub struct ExportMetadata {
/// (Optional) File extension of the export. For example "lss"
pub extension: Option<String>,

/// Extra properties to pass to the exporter when running
///
/// This can be used to distinguish multiple exports from the same exporter.
/// This is not part of the config and cannot be changed by the user
pub properties: Value,
/// (Optional) Extra id to distinguish multiple exports from the same exporter
pub export_id: Option<String>,

/// (Optional) Example YAML configuration for the exporter to show to the user
pub example_config: Option<String>,

/// (Optional) Learn more link to guide the user on how to configure the export.
///
/// ONLY VISIBLE if you also provide an example config!
pub learn_more: Option<String>,
}

/// Icon for the export.
Expand All @@ -68,14 +71,41 @@ pub enum ExportIcon {
Video,
}

/// Request to export a document sent from the client
#[derive(Debug, Clone)]
#[derive_wasm]
pub struct ExportRequest {
/// Id of the exporter plugin to run
#[serde(rename = "pluginId")]
pub plugin_id: String,
/// Extra id to distinguish multiple exports from the same exporter
#[serde(rename = "exportId")]
pub export_id: String,
/// Configuration payload provided by the user
pub payload: Value,
}

/// The exported document type
#[derive(Debug, Clone)]
#[derive_wasm]
pub struct ExpoDoc {
/// The file name
pub file_name: String,
/// The content of the file
pub bytes: Vec<u8>,
pub enum ExpoDoc {
/// Success output. Contains file name and the bytes
Success {
file_name: String,
file_content: ExpoBlob,
},
/// Error output with a message
Error(String),
}

/// The data in the export
#[derive(Debug, Clone)]
#[derive_wasm]
pub enum ExpoBlob {
/// UTF-8 text
Text(String),
/// Binary data encoded in base64
Base64(String),
}

impl<'p> ExecContext<'p> {
Expand All @@ -92,3 +122,51 @@ impl<'p> ExecContext<'p> {
}
}
}

impl<'p> CompDoc<'p> {
/// Run the export request on this document after the comp phase
///
/// Returning `Some` means the export was successful. Returning `None` means the export is
/// pending and needed to run in the exec phase
pub fn run_exporter(&mut self, req: &ExportRequest) -> Option<ExpoDoc> {
let mut plugins = std::mem::take(&mut self.plugin_runtimes);

for plugin in &mut plugins {
if req.plugin_id == plugin.get_id() {
let result = match plugin.on_export_comp_doc(&req.export_id, &req.payload, self) {
Ok(None) => None,
Ok(Some(expo_doc)) => Some(expo_doc),
Err(e) => Some(ExpoDoc::Error(e.to_string())),
};
self.plugin_runtimes = plugins;
return result;
}
}

self.plugin_runtimes = plugins;
Some(ExpoDoc::Error(format!(
"Plugin {} not found",
req.plugin_id
)))
}
}

impl<'p> ExecContext<'p> {
/// Run the export request on this document after the exec phase
pub fn run_exporter(self, req: ExportRequest) -> ExpoDoc {
let mut plugins = self.plugin_runtimes;

for plugin in &mut plugins {
if req.plugin_id == plugin.get_id() {
let result =
match plugin.on_export_exec_doc(&req.export_id, req.payload, &self.exec_doc) {
Ok(expo_doc) => expo_doc,
Err(e) => ExpoDoc::Error(e.to_string()),
};
return result;
}
}

ExpoDoc::Error(format!("Plugin {} not found", req.plugin_id))
}
}
2 changes: 1 addition & 1 deletion compiler-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub mod prep;
// public API re-exports
pub use comp::CompDoc;
pub use exec::{ExecContext, ExecDoc};
pub use expo::{ExpoContext, ExpoDoc};
pub use expo::{ExpoContext, ExpoDoc, ExportRequest};
pub use pack::{CompileContext, Compiler};
pub use prep::{ContextBuilder, PreparedContext};

Expand Down
5 changes: 2 additions & 3 deletions compiler-core/src/plugin/builtin/livesplit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

use std::borrow::Cow;

use serde_json::json;

use crate::expo::{ExportIcon, ExportMetadata};

use crate::plugin::{PluginResult, PluginRuntime};
Expand All @@ -22,8 +20,9 @@ impl PluginRuntime for ExportLiveSplitPlugin {
description: "Export to a LiveSplit split file".to_string(),
icon: ExportIcon::Data,
extension: Some("lss".to_string()),
properties: json!(null),
export_id: None,
example_config: Some(include_str!("./livesplit.yaml").to_string()),
learn_more: Some("/docs/plugin/export-livesplit".to_string()),
};
Ok(Some(vec![metadata]))
}
Expand Down
4 changes: 2 additions & 2 deletions compiler-core/src/plugin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ pub trait PluginRuntime {
/// with the ExecDoc
fn on_export_comp_doc(
&mut self,
_properties: &Value,
_export_id: &str,
_payload: &Value,
_doc: &CompDoc,
) -> PluginResult<Option<ExpoDoc>> {
Expand All @@ -91,7 +91,7 @@ pub trait PluginRuntime {
/// The exporter must return the export data or throw an error
fn on_export_exec_doc(
&mut self,
_properties: Value,
_export_id: &str,
_payload: Value,
_doc: &ExecDoc,
) -> PluginResult<ExpoDoc> {
Expand Down
14 changes: 9 additions & 5 deletions compiler-core/src/prep/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ where
L: Loader,
{
pub project_res: Resource<'static, L>,
pub entry_path: Option<String>,
pub config: RouteConfig,
pub meta: CompilerMetadata,
pub prep_doc: PrepDoc,
Expand Down Expand Up @@ -220,6 +221,7 @@ where

Ok(PreparedContext {
project_res: self.project_res,
entry_path: self.entry_point,
config,
meta,
prep_doc,
Expand Down Expand Up @@ -248,6 +250,7 @@ where
}

/// Load the project and switch the project resource to the entry point resource.
/// Also sets self.entry_point to the resolved entry path.
/// Returns the loaded project object with the `entry-points` property removed
///
/// If the entry point is None, it will attempt to redirect to the "default" entry point
Expand All @@ -270,25 +273,26 @@ where
};

if let Some(redirect_path) = path {
match Use::new(redirect_path) {
return match Use::new(redirect_path.clone()) {
Use::Valid(valid) if matches!(valid, ValidUse::Absolute(_)) => {
// since the path is absolute, we can just use the project_res to resolve
// it
self.entry_point = Some(redirect_path.to_string());
self.project_res = self.project_res.resolve(&valid)?;
let mut project_obj = self.load_project().await?;
// remove and ignore the entry points in the redirected project
project_obj.remove(prop::ENTRY_POINTS);
return Ok(project_obj);
Ok(project_obj)
}
_ => {
// this shouldn't happen
// since load_entry_points checks for if the path is valid
return Err(PrepError::InvalidEntryPoint(
Err(PrepError::InvalidEntryPoint(
self.entry_point.as_ref().cloned().unwrap_or_default(),
"unreachable".to_string(),
));
))
}
}
};
}
}

Expand Down
62 changes: 62 additions & 0 deletions compiler-wasm/src/compiler/cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use std::cell::RefCell;

use log::info;

use celerc::PreparedContext;

use crate::loader::{self, LoadFileOutput, LoaderInWasm};

thread_local! {
static CACHED_COMPILER_CONTEXT: RefCell<Option<PreparedContext<LoaderInWasm>>> = RefCell::new(None);
}

/// Guard for acquiring the cached context and takes care of releasing it
pub struct CachedContextGuard(Option<PreparedContext<LoaderInWasm>>);
impl CachedContextGuard {
/// Acquire the cached context if it's valid
pub async fn acquire(entry_path: Option<&String>) -> Option<Self> {
match CACHED_COMPILER_CONTEXT.with_borrow_mut(|x| x.take()) {
Some(prep_ctx) => {
// check if cache is still valid
if prep_ctx.entry_path.as_ref() != entry_path {
info!("invalidating compiler cache because entry path changed");
return None;
}
// TODO #173: prep phase need to output local dependencies
let mut dependencies = vec!["/project.yaml".to_string()];
if let Some(entry_path) = entry_path {
dependencies.push(entry_path.clone());
}
for dep in &dependencies {
// strip leading slash if needed
let dep_path = match dep.strip_prefix('/') {
Some(x) => x,
None => dep,
};
let changed = loader::load_file_check_changed(dep_path).await;
if !matches!(changed, Ok(LoadFileOutput::NotModified)) {
info!("invalidating compiler cache because dependency changed: {dep}");
return None;
}
}
Some(CachedContextGuard(Some(prep_ctx)))
}
None => None,
}
}

/// Put a new context into the cache upon drop
pub fn new(prep_ctx: PreparedContext<LoaderInWasm>) -> Self {
CachedContextGuard(Some(prep_ctx))
}
}
impl Drop for CachedContextGuard {
fn drop(&mut self) {
CACHED_COMPILER_CONTEXT.with_borrow_mut(|x| *x = self.0.take());
}
}
impl AsRef<PreparedContext<LoaderInWasm>> for CachedContextGuard {
fn as_ref(&self) -> &PreparedContext<LoaderInWasm> {
self.0.as_ref().unwrap()
}
}
79 changes: 79 additions & 0 deletions compiler-wasm/src/compiler/export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use instant::Instant;
use log::{error, info};
use wasm_bindgen::prelude::*;

use celerc::pack::PackError;
use celerc::{Compiler, ExpoDoc, ExportRequest, PluginOptions, PreparedContext};

use crate::loader::LoaderInWasm;
use crate::plugin;

use super::CachedContextGuard;

pub async fn export_document(
entry_path: Option<String>,
use_cache: bool,
req: ExportRequest,
) -> Result<ExpoDoc, JsValue> {
info!("exporting document");
let plugin_options = match plugin::get_plugin_options() {
Ok(x) => x,
Err(message) => {
let message = format!("Failed to load user plugin options: {message}");
error!("{message}");
return Ok(ExpoDoc::Error(message));
}
};

if use_cache {
if let Some(guard) = CachedContextGuard::acquire(entry_path.as_ref()).await {
info!("using cached compiler context");
let start_time = Instant::now();
return export_in_context(guard.as_ref(), Some(start_time), plugin_options, req).await;
}
}

// create a new context
info!("creating new compiler context");
let start_time = Instant::now();
let prep_ctx = match super::new_context(entry_path).await {
Ok(x) => x,
Err(e) => {
return Ok(ExpoDoc::Error(e.to_string()));
}
};
let guard = CachedContextGuard::new(prep_ctx);
export_in_context(guard.as_ref(), Some(start_time), plugin_options, req).await
}

async fn export_in_context(
prep_ctx: &PreparedContext<LoaderInWasm>,
start_time: Option<Instant>,
plugin_options: Option<PluginOptions>,
req: ExportRequest,
) -> Result<ExpoDoc, JsValue> {
let mut comp_ctx = prep_ctx.new_compilation(start_time).await;
match comp_ctx.configure_plugins(plugin_options).await {
Err(e) => export_with_pack_error(e),
Ok(_) => match prep_ctx.create_compiler(comp_ctx).await {
Ok(x) => export_with_compiler(x, req).await,
Err((e, _)) => export_with_pack_error(e),
},
}
}

fn export_with_pack_error(error: PackError) -> Result<ExpoDoc, JsValue> {
Ok(ExpoDoc::Error(error.to_string()))
}

async fn export_with_compiler(
compiler: Compiler<'_>,
req: ExportRequest,
) -> Result<ExpoDoc, JsValue> {
let mut comp_doc = compiler.compile().await;
if let Some(expo_doc) = comp_doc.run_exporter(&req) {
return Ok(expo_doc);
}
let exec_ctx = comp_doc.execute().await;
Ok(exec_ctx.run_exporter(req))
}
Loading

0 comments on commit 5ccb484

Please sign in to comment.