Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export config dialog #185

Merged
merged 5 commits into from
Feb 10, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading