From 61406b8d623a50807345f94bbbc65eed4a13a78b Mon Sep 17 00:00:00 2001 From: Pistonight Date: Sun, 4 Feb 2024 14:52:22 -0800 Subject: [PATCH 1/5] compiler wasm export API; start working on export dialog --- compiler-core/src/expo/mod.rs | 72 +++++++++- compiler-core/src/lib.rs | 2 +- compiler-core/src/prep/mod.rs | 13 +- compiler-wasm/src/compiler/cache.rs | 97 +++++++++++++ compiler-wasm/src/compiler/export.rs | 76 ++++++++++ .../src/{compile.rs => compiler/mod.rs} | 105 ++++++++------ compiler-wasm/src/lib.rs | 19 ++- .../core/doc/useDocCurrentUserPluginConfig.ts | 11 +- web-client/src/core/kernel/AlertMgr.ts | 87 ++++++++++-- .../core/kernel/compiler/CompilerKernel.ts | 7 +- .../kernel/compiler/CompilerKernelImpl.ts | 130 +++++++++++++----- web-client/src/low/utils/error.ts | 13 ++ web-client/src/low/utils/index.ts | 1 + web-client/src/ui/app/AppAlert.tsx | 22 +-- web-client/src/ui/toolbar/Export.tsx | 112 ++++++++++++--- 15 files changed, 622 insertions(+), 145 deletions(-) create mode 100644 compiler-wasm/src/compiler/cache.rs create mode 100644 compiler-wasm/src/compiler/export.rs rename compiler-wasm/src/{compile.rs => compiler/mod.rs} (57%) create mode 100644 web-client/src/low/utils/error.ts diff --git a/compiler-core/src/expo/mod.rs b/compiler-core/src/expo/mod.rs index 148cb770..0e03f6de 100644 --- a/compiler-core/src/expo/mod.rs +++ b/compiler-core/src/expo/mod.rs @@ -11,6 +11,7 @@ use serde_json::Value; use crate::exec::ExecContext; +use crate::comp::CompDoc; use crate::macros::derive_wasm; /// Output of the export phase @@ -68,14 +69,31 @@ 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 + pub plugin_id: String, + /// Extra properties to pass back to the exporter. + pub properties: Value, + /// 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, +pub enum ExpoDoc { + /// Success output. Contains file name and the bytes + Success { + /// File name + file_name: String, + /// File bytes + bytes: Vec, + }, + /// Error output with a message + Error(String), } impl<'p> ExecContext<'p> { @@ -92,3 +110,47 @@ 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 { + 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.properties, &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.properties, 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)) + } +} diff --git a/compiler-core/src/lib.rs b/compiler-core/src/lib.rs index b6db81f0..11ef7b05 100644 --- a/compiler-core/src/lib.rs +++ b/compiler-core/src/lib.rs @@ -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}; diff --git a/compiler-core/src/prep/mod.rs b/compiler-core/src/prep/mod.rs index 13b03729..8d3312eb 100644 --- a/compiler-core/src/prep/mod.rs +++ b/compiler-core/src/prep/mod.rs @@ -49,6 +49,7 @@ where L: Loader, { pub project_res: Resource<'static, L>, + pub entry_path: Option, pub config: RouteConfig, pub meta: CompilerMetadata, pub prep_doc: PrepDoc, @@ -220,6 +221,7 @@ where Ok(PreparedContext { project_res: self.project_res, + entry_path: self.entry_point, config, meta, prep_doc, @@ -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 @@ -269,24 +272,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(), - )); + )) } } } diff --git a/compiler-wasm/src/compiler/cache.rs b/compiler-wasm/src/compiler/cache.rs new file mode 100644 index 00000000..fb711957 --- /dev/null +++ b/compiler-wasm/src/compiler/cache.rs @@ -0,0 +1,97 @@ + +use std::cell::RefCell; +use std::ops::{Deref, DerefMut}; + +use celerc::lang::DocDiagnostic; +use instant::Instant; +use log::{error, info}; +use wasm_bindgen::prelude::*; + +use celerc::pack::PackError; +use celerc::{ + CompDoc, CompileContext, Compiler, ContextBuilder, ExecContext, PluginOptions, PreparedContext, +}; + +use crate::interop::OpaqueExpoContext; +use crate::loader::{self, LoadFileOutput, LoaderInWasm}; +use crate::plugin; + +thread_local! { + static CACHED_COMPILER_CONTEXT: RefCell>> = RefCell::new(None); +} + +/// Guard for acquiring the cached context and takes care of releasing it +pub struct CachedContextGuard(Option>); +impl CachedContextGuard { + /// Acquire the cached context if it's valid + pub async fn acquire(entry_path: Option<&String>) -> Option { + 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) -> 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> for CachedContextGuard { + fn as_ref(&self) -> &PreparedContext { + self.0.as_ref().unwrap() + } +} + +// pub async fn is_cache_valid(entry_path: Option<&String>) -> bool { +// let root_project_result = loader::load_file_check_changed("project.yaml").await; +// if !matches!(root_project_result, Ok(LoadFileOutput::NotModified)) { +// info!("root project.yaml is modified"); +// return false; +// } +// if let Some(entry_path) = entry_path { +// let entry_path = match entry_path.strip_prefix('/') { +// Some(x) => x, +// None => entry_path, +// }; +// let entry_result = loader::load_file_check_changed(entry_path).await; +// if !matches!(entry_result, Ok(LoadFileOutput::NotModified)) { +// info!("entry project.yaml is modified"); +// return false; +// } +// } +// let is_same = CACHED_COMPILER_ENTRY_PATH.with_borrow(|x| x.as_ref() == entry_path); +// if !is_same { +// info!("entry changed"); +// return false; +// } +// } + diff --git a/compiler-wasm/src/compiler/export.rs b/compiler-wasm/src/compiler/export.rs new file mode 100644 index 00000000..2c433737 --- /dev/null +++ b/compiler-wasm/src/compiler/export.rs @@ -0,0 +1,76 @@ +use instant::Instant; +use wasm_bindgen::prelude::*; +use log::{error, info}; + +use celerc::{ExpoDoc, ExportRequest, PreparedContext, PluginOptions, Compiler}; +use celerc::pack::PackError; + +use crate::plugin; +use crate::loader::LoaderInWasm; + +use super::CachedContextGuard; + +pub async fn export_document( + entry_path: Option, + use_cache: bool, + req: ExportRequest, +) -> Result { + 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, + start_time: Option, + plugin_options: Option, + req: ExportRequest, +) -> Result { + 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 { + Ok(ExpoDoc::Error(error.to_string())) +} + +async fn export_with_compiler(compiler: Compiler<'_>, req: ExportRequest) -> Result { + 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)) +} diff --git a/compiler-wasm/src/compile.rs b/compiler-wasm/src/compiler/mod.rs similarity index 57% rename from compiler-wasm/src/compile.rs rename to compiler-wasm/src/compiler/mod.rs index 1bdfb5d0..1c9e4533 100644 --- a/compiler-wasm/src/compile.rs +++ b/compiler-wasm/src/compiler/mod.rs @@ -1,6 +1,8 @@ use std::cell::RefCell; +use std::ops::{Deref, DerefMut}; use celerc::lang::DocDiagnostic; +use celerc::prep::PrepResult; use instant::Instant; use log::{error, info}; use wasm_bindgen::prelude::*; @@ -14,10 +16,10 @@ use crate::interop::OpaqueExpoContext; use crate::loader::{self, LoadFileOutput, LoaderInWasm}; use crate::plugin; -thread_local! { - static CACHED_COMPILER_CONTEXT: RefCell>> = RefCell::new(None); - static CACHED_COMPILER_ENTRY_PATH: RefCell> = RefCell::new(None); -} +mod cache; +use cache::CachedContextGuard; +mod export; +pub use export::export_document; /// Compile a document from web editor pub async fn compile_document( @@ -35,25 +37,33 @@ pub async fn compile_document( } }; - if use_cache && is_cached_compiler_valid(entry_path.as_ref()).await { - let cached_context = CACHED_COMPILER_CONTEXT.with_borrow_mut(|x| x.take()); - - if let Some(prep_ctx) = cached_context { + if use_cache { + if let Some(guard) = CachedContextGuard::acquire(entry_path.as_ref()).await { info!("using cached compiler context"); let start_time = Instant::now(); - let result = compile_in_context(&prep_ctx, Some(start_time), plugin_options).await; - CACHED_COMPILER_CONTEXT.with_borrow_mut(|x| x.replace(prep_ctx)); - return result; + return compile_in_context(guard.as_ref(), Some(start_time), plugin_options).await; } } + + + // if use_cache && is_cached_compiler_valid(entry_path.as_ref()).await { + // let cached_context = CACHED_COMPILER_CONTEXT.with_borrow_mut(|x| x.take()); + // + // if let Some(prep_ctx) = cached_context { + // info!("using cached compiler context"); + // let start_time = Instant::now(); + // let result = compile_in_context(&prep_ctx, Some(start_time), plugin_options).await; + // CACHED_COMPILER_CONTEXT.with_borrow_mut(|x| x.replace(prep_ctx)); + // return result; + // } + // } + // create a new context - let mut context_builder = new_context_builder(); - if entry_path.is_some() { - context_builder = context_builder.entry_point(entry_path); - } + info!("creating new compiler context"); let start_time = Instant::now(); - let prep_ctx = match context_builder.build_context().await { + + let prep_ctx = match new_context(entry_path).await { Ok(x) => x, Err(e) => { let comp_doc = CompDoc::from_prep_error(e, start_time); @@ -61,40 +71,47 @@ pub async fn compile_document( return OpaqueExpoContext::try_from(exec_context.prepare_exports()); } }; + let guard = CachedContextGuard::new(prep_ctx); - let result = compile_in_context(&prep_ctx, None, plugin_options).await; - CACHED_COMPILER_CONTEXT.with_borrow_mut(|x| x.replace(prep_ctx)); - result + compile_in_context(guard.as_ref(), None, plugin_options).await } -async fn is_cached_compiler_valid(entry_path: Option<&String>) -> bool { - // TODO #173: better cache invalidation when local config changes - - let root_project_result = loader::load_file_check_changed("project.yaml").await; - if !matches!(root_project_result, Ok(LoadFileOutput::NotModified)) { - info!("root project.yaml is modified"); - return false; - } - if let Some(entry_path) = entry_path { - let entry_path = match entry_path.strip_prefix('/') { - Some(x) => x, - None => entry_path, - }; - let entry_result = loader::load_file_check_changed(entry_path).await; - if !matches!(entry_result, Ok(LoadFileOutput::NotModified)) { - info!("entry project.yaml is modified"); - return false; - } - } - let is_same = CACHED_COMPILER_ENTRY_PATH.with_borrow(|x| x.as_ref() == entry_path); - if !is_same { - info!("entry changed"); - return false; +pub async fn new_context(entry_path: Option) -> PrepResult> { + let mut context_builder = new_context_builder(); + if entry_path.is_some() { + context_builder = context_builder.entry_point(entry_path); } - - true + context_builder.build_context().await } +// async fn is_cached_compiler_valid(entry_path: Option<&String>) -> bool { +// // TODO #173: better cache invalidation when local config changes +// +// let root_project_result = loader::load_file_check_changed("project.yaml").await; +// if !matches!(root_project_result, Ok(LoadFileOutput::NotModified)) { +// info!("root project.yaml is modified"); +// return false; +// } +// if let Some(entry_path) = entry_path { +// let entry_path = match entry_path.strip_prefix('/') { +// Some(x) => x, +// None => entry_path, +// }; +// let entry_result = loader::load_file_check_changed(entry_path).await; +// if !matches!(entry_result, Ok(LoadFileOutput::NotModified)) { +// info!("entry project.yaml is modified"); +// return false; +// } +// } +// let is_same = CACHED_COMPILER_ENTRY_PATH.with_borrow(|x| x.as_ref() == entry_path); +// if !is_same { +// info!("entry changed"); +// return false; +// } +// +// true +// } + async fn compile_in_context( prep_ctx: &PreparedContext, start_time: Option, diff --git a/compiler-wasm/src/lib.rs b/compiler-wasm/src/lib.rs index dfec595a..b0cc8e47 100644 --- a/compiler-wasm/src/lib.rs +++ b/compiler-wasm/src/lib.rs @@ -1,4 +1,4 @@ -use celerc::PluginOptionsRaw; +use celerc::{PluginOptionsRaw, ExportRequest, ExpoDoc}; use js_sys::Function; use log::info; use wasm_bindgen::prelude::*; @@ -9,7 +9,7 @@ use celerc::res::{ResPath, Resource}; mod interop; use interop::OpaqueExpoContext; -mod compile; +mod compiler; mod loader; use loader::LoaderInWasm; mod logger; @@ -39,7 +39,7 @@ pub fn init( /// If there is any error, this returns 0 entry points #[wasm_bindgen] pub async fn get_entry_points() -> Result { - let context_builder = compile::new_context_builder(); + let context_builder = compiler::new_context_builder(); let entry_points = match context_builder.get_entry_points().await { Ok(x) => x.path_only().into(), Err(_) => Default::default(), @@ -54,7 +54,18 @@ pub async fn compile_document( entry_path: Option, use_cache: bool, ) -> Result { - compile::compile_document(entry_path, use_cache).await + compiler::compile_document(entry_path, use_cache).await +} + +/// Export a document from web editor +#[wasm_bindgen] +#[inline] +pub async fn export_document( + entry_path: Option, + use_cache: bool, + req: ExportRequest +) -> Result { + compiler::export_document(entry_path, use_cache, req).await } /// Set user plugin options diff --git a/web-client/src/core/doc/useDocCurrentUserPluginConfig.ts b/web-client/src/core/doc/useDocCurrentUserPluginConfig.ts index ff179837..e974dd60 100644 --- a/web-client/src/core/doc/useDocCurrentUserPluginConfig.ts +++ b/web-client/src/core/doc/useDocCurrentUserPluginConfig.ts @@ -4,6 +4,7 @@ import YAML from "js-yaml"; import { documentSelector, settingsSelector } from "core/store"; import { ExecDoc, Value } from "low/celerc"; +import { errorToString } from "low/utils"; type UserPluginOptionsResult = [Value[], undefined] | [undefined, string]; @@ -61,14 +62,6 @@ export const parseUserConfigOptions = ( } return [options, undefined]; } catch (e) { - if (typeof e === "string") { - return [undefined, e]; - } - if (e && typeof e === "object" && "message" in e) { - if (typeof e.message === "string") { - return [undefined, e.message]; - } - } - return [undefined, `${e}`]; + return [undefined, errorToString(e)]; } }; diff --git a/web-client/src/core/kernel/AlertMgr.ts b/web-client/src/core/kernel/AlertMgr.ts index 648e1151..d2af53cf 100644 --- a/web-client/src/core/kernel/AlertMgr.ts +++ b/web-client/src/core/kernel/AlertMgr.ts @@ -2,6 +2,7 @@ import { AlertExtraAction, ModifyAlertActionPayload } from "core/stage"; import { AppDispatcher, viewActions } from "core/store"; +import { Result, allocErr, allocOk } from "low/utils"; /// Options for showing a simple alert export type AlertOptions = { @@ -33,6 +34,16 @@ export type RichAlertOptions = { extraActions?: TExtra; }; +/// Options to show a blocking alert while another operation is running +export type BlockingAlertOptions = { + /// Title of the alert + title: string; + /// Body component of the alert + component: React.ComponentType; + /// Text for the cancel button. Default is "Cancel" + cancelButton?: string; +} + type IdsOf = T[number]["id"]; type AlertCallback = (ok: boolean | string) => void; @@ -101,6 +112,50 @@ export class AlertMgr { }); } + /// Show a blocking alert and run f + /// + /// The promise will resolve to the result of f, or Err(false) if the user + /// cancels. + /// + /// If f throws, the alert will be cleared, and Err(e) will be returned. + public showBlocking({ + title, + component, + cancelButton, + }: BlockingAlertOptions, f: () => Promise): Promise> { + return new Promise((resolve) => { + let cancelled = false; + this.initAlert(() => { + // when alert is notified through user action, + // it means cancel + cancelled = true; + resolve(allocErr(false)); + }, component); + this.store.dispatch( + viewActions.setAlert({ + title, + text: "", + okButton: "", + cancelButton: cancelButton ?? "Cancel", + learnMore: "", + extraActions: [], + }), + ); + // let the UI update first + setTimeout(() => { + f().then((result) => { + if (!cancelled) { + this.clearAlertAndThen(() => resolve(allocOk(result))); + } + }).catch(e => { + if (!cancelled) { + this.clearAlertAndThen(() => resolve(allocErr(e))); + } + }); + }, ALERT_TIMEOUT); + }); + } + /// Modify the current alert's actions /// /// Only effective if a dialog is showing @@ -116,24 +171,28 @@ export class AlertMgr { ) { this.previousFocusedElement = document.activeElement ?? undefined; this.alertCallback = (response) => { - this.store.dispatch(viewActions.clearAlert()); - this.alertCallback = undefined; - this.RichAlertComponent = undefined; - setTimeout(() => { - const element = this.previousFocusedElement; - if ( - element && - "focus" in element && - typeof element.focus === "function" - ) { - element.focus(); - } - resolve(response); - }, ALERT_TIMEOUT); + this.clearAlertAndThen(() => resolve(response)); }; this.RichAlertComponent = component; } + private clearAlertAndThen(cb: () => void) { + this.store.dispatch(viewActions.clearAlert()); + this.alertCallback = undefined; + this.RichAlertComponent = undefined; + setTimeout(() => { + const element = this.previousFocusedElement; + if ( + element && + "focus" in element && + typeof element.focus === "function" + ) { + element.focus(); + } + cb(); + }, ALERT_TIMEOUT); + } + /// Called from the alert dialog to notify the user action public onAction(response: boolean | string) { this.alertCallback?.(response); diff --git a/web-client/src/core/kernel/compiler/CompilerKernel.ts b/web-client/src/core/kernel/compiler/CompilerKernel.ts index 648f311d..99919f20 100644 --- a/web-client/src/core/kernel/compiler/CompilerKernel.ts +++ b/web-client/src/core/kernel/compiler/CompilerKernel.ts @@ -1,4 +1,4 @@ -import { EntryPointsSorted } from "low/celerc"; +import { EntryPointsSorted, ExpoDoc, ExportRequest } from "low/celerc"; import { FileAccess } from "low/fs"; import { Result } from "low/utils"; @@ -25,4 +25,9 @@ export interface CompilerKernel { /// Get compiler entry points getEntryPoints(): Promise>; + + /// Export the document with the given request + /// + /// Any error will be stored in the return value. This function will not throw + export(request: ExportRequest): Promise; } diff --git a/web-client/src/core/kernel/compiler/CompilerKernelImpl.ts b/web-client/src/core/kernel/compiler/CompilerKernelImpl.ts index 54292d32..aeacbbc3 100644 --- a/web-client/src/core/kernel/compiler/CompilerKernelImpl.ts +++ b/web-client/src/core/kernel/compiler/CompilerKernelImpl.ts @@ -8,8 +8,11 @@ import { } from "core/store"; import { EntryPointsSorted, + ExpoDoc, + ExportRequest, PluginOptionsRaw, compile_document, + export_document, get_entry_points, set_plugin_options, } from "low/celerc"; @@ -22,6 +25,7 @@ import { Result, sleep, allocErr, + ReentrantLock, } from "low/utils"; import { FileAccess, FsResultCodes } from "low/fs"; @@ -51,7 +55,10 @@ export class CompilerKernelImpl implements CompilerKernel { private fileAccess: FileAccess | undefined = undefined; private needCompile: boolean; + /// Flag used to prevent multiple compilation to run at the same time private compiling: boolean; + /// Lock to prevent compilation and other operations from running at the same time + private compilerLock: ReentrantLock; private lastPluginOptions: PluginOptionsRaw | undefined; private cleanup: () => void; @@ -60,6 +67,7 @@ export class CompilerKernelImpl implements CompilerKernel { this.store = store; this.needCompile = false; this.compiling = false; + this.compilerLock = new ReentrantLock("compiler"); this.cleanup = () => { // no cleanup needed for now @@ -165,55 +173,92 @@ export class CompilerKernelImpl implements CompilerKernel { this.store.dispatch(viewActions.setCompileInProgress(true)); - // wait to let the UI update first - await sleep(0); - // check if another compilation is running - // this is safe because there's no await between checking and setting (no other code can run) - if (this.compiling) { - CompilerLog.warn("compilation already in progress, skipping"); - return; - } - this.compiling = true; - while (this.needCompile) { - // turn off the flag before compiling. - // if anyone calls triggerCompile during compilation, it will be turned on again - // to trigger another compile - this.needCompile = false; - const state = this.store.getState(); - const { compilerUseCachedPrepPhase } = settingsSelector(state); - - const pluginOptions = getRawPluginOptions(state); - if (pluginOptions !== this.lastPluginOptions) { - this.lastPluginOptions = pluginOptions; + // lock the compiler so other operations can't run + await this.compilerLock.lockedScope(undefined, async () => { + // wait to let the UI update first + await sleep(0); + // check if another compilation is running + // this is safe because there's no await between checking and setting (no other code can run) + if (this.compiling) { + CompilerLog.warn("compilation already in progress, skipping"); + return; + } + this.compiling = true; + while (this.needCompile) { + // turn off the flag before compiling. + // if anyone calls triggerCompile during compilation, it will be turned on again + // to trigger another compile + this.needCompile = false; + const state = this.store.getState(); + const { compilerUseCachedPrepPhase } = settingsSelector(state); + + await this.updatePluginOptions(); + + CompilerLog.info("invoking compiler..."); const result = await wrapAsync(() => { - return set_plugin_options(pluginOptions); + return compile_document( + validatedEntryPath, + compilerUseCachedPrepPhase, + ); }); + // yielding just in case other things need to update + await sleep(0); if (result.isErr()) { CompilerLog.error(result.inner()); + } else { + const doc = result.inner(); + if (this.fileAccess && doc !== undefined) { + this.store.dispatch(documentActions.setDocument(doc)); + } } } + this.store.dispatch(viewActions.setCompileInProgress(false)); + this.compiling = false; + CompilerLog.info("finished compiling"); + }); + } + + public async export(request: ExportRequest): Promise { + if (!this.fileAccess) { + return { + error: "Compiler not available. Please make sure a project is loaded." + }; + } + + if (!(await this.ensureReady())) { + return { + error: "Compiler is not ready. Please try again later." + }; + } + + const validatedEntryPathResult = await this.validateEntryPath(); + if (validatedEntryPathResult.isErr()) { + return { + error: "Compiler entry path is invalid. Please check your settings." + }; + } + const validatedEntryPath = validatedEntryPathResult.inner(); + + return await this.compilerLock.lockedScope(undefined, async () => { + const { compilerUseCachedPrepPhase } = settingsSelector(this.store.getState()); + + await this.updatePluginOptions(); - CompilerLog.info("invoking compiler..."); const result = await wrapAsync(() => { - return compile_document( + return export_document( validatedEntryPath, compilerUseCachedPrepPhase, + request, ); }); - // yielding just in case other things need to update - await sleep(0); + if (result.isErr()) { CompilerLog.error(result.inner()); - } else { - const doc = result.inner(); - if (this.fileAccess && doc !== undefined) { - this.store.dispatch(documentActions.setDocument(doc)); - } + return { error: `${result.inner()}` }; } - } - this.store.dispatch(viewActions.setCompileInProgress(false)); - this.compiling = false; - CompilerLog.info("finished compiling"); + return result.inner(); + }); + } /// Try to wait for the compiler to be ready. Returns true if it becomes ready eventually. @@ -326,4 +371,21 @@ export class CompilerKernelImpl implements CompilerKernel { } return ""; } + + private async updatePluginOptions() { + const pluginOptions = getRawPluginOptions(this.store.getState()); + if (pluginOptions !== this.lastPluginOptions) { + this.lastPluginOptions = pluginOptions; + CompilerLog.info("updating plugin options..."); + const result = await wrapAsync(() => { + return set_plugin_options(pluginOptions); + }); + if (result.isErr()) { + CompilerLog.error(result.inner()); + CompilerLog.warn("failed to set plugin options. The output may be wrong."); + } else { + CompilerLog.info("plugin options updated"); + } + } + } } diff --git a/web-client/src/low/utils/error.ts b/web-client/src/low/utils/error.ts new file mode 100644 index 00000000..66756897 --- /dev/null +++ b/web-client/src/low/utils/error.ts @@ -0,0 +1,13 @@ +//! Error utilities + +export function errorToString(e: unknown): string { + if (typeof e === "string") { + return e; + } + if (e && typeof e === "object" && "message" in e) { + if (typeof e.message === "string") { + return e.message; + } + } + return `${e}`; +} diff --git a/web-client/src/low/utils/index.ts b/web-client/src/low/utils/index.ts index 070893db..939f15b2 100644 --- a/web-client/src/low/utils/index.ts +++ b/web-client/src/low/utils/index.ts @@ -4,6 +4,7 @@ export * from "./IdleMgr"; export * from "./Debouncer"; +export * from "./error"; export * from "./html"; export * from "./Logger"; export * from "./Pool"; diff --git a/web-client/src/ui/app/AppAlert.tsx b/web-client/src/ui/app/AppAlert.tsx index ea20d3f2..66a57fcb 100644 --- a/web-client/src/ui/app/AppAlert.tsx +++ b/web-client/src/ui/app/AppAlert.tsx @@ -69,16 +69,18 @@ export const AppAlert: React.FC = () => { )} 0}> - - - + {alertOkButton && ( + + + + )} {alertCancelButton && ( + + } + ); } diff --git a/web-client/src/ui/toolbar/settings/MapSettings.tsx b/web-client/src/ui/toolbar/settings/MapSettings.tsx index 8d5f4048..4ea5fe81 100644 --- a/web-client/src/ui/toolbar/settings/MapSettings.tsx +++ b/web-client/src/ui/toolbar/settings/MapSettings.tsx @@ -235,29 +235,17 @@ const SectionModeSelector: React.FC = ({ setValue(data.selectedOptions[0] as SectionMode); }} > - - - - @@ -301,23 +289,14 @@ const LayerModeSelector: React.FC = ({ setValue(data.selectedOptions[0] as LayerMode); }} > - - - diff --git a/web-client/src/ui/toolbar/settings/PluginSettings.tsx b/web-client/src/ui/toolbar/settings/PluginSettings.tsx index bab282ff..74506e2f 100644 --- a/web-client/src/ui/toolbar/settings/PluginSettings.tsx +++ b/web-client/src/ui/toolbar/settings/PluginSettings.tsx @@ -101,18 +101,13 @@ export const PluginSettings: React.FC = () => { Configure extra plugins to use when loading route documents.{" "} Learn more } - hint={ - document - ? `The current document title is "${document.project.title}"` - : undefined - } > = ({ Please edit your plugin configuration below.{" "} Learn more From f5309442a80455abdf9ffeddd3b0ed26fdbe5941 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Sat, 10 Feb 2024 05:43:05 -0800 Subject: [PATCH 3/5] clean up --- compiler-core/src/expo/mod.rs | 22 +- compiler-core/src/prep/mod.rs | 3 +- compiler-wasm/src/compiler/cache.rs | 17 +- compiler-wasm/src/compiler/export.rs | 11 +- compiler-wasm/src/compiler/mod.rs | 47 +---- compiler-wasm/src/lib.rs | 4 +- docs/src/plugin/built-in.md | 1 + docs/src/plugin/export-livesplit.md | 2 + web-client/src/core/doc/export.ts | 27 ++- web-client/src/core/doc/settingsReducers.ts | 20 +- web-client/src/core/kernel/AlertMgr.ts | 29 +-- web-client/src/core/kernel/Kernel.ts | 8 +- .../kernel/compiler/CompilerKernelImpl.ts | 15 +- web-client/src/low/utils/FileSaver/index.ts | 5 +- web-client/src/low/utils/WorkerHost.ts | 4 +- .../ui/shared/PrismEditor/PrismEditorCore.tsx | 15 +- web-client/src/ui/toolbar/Export.tsx | 198 +++++++++++------- .../src/ui/toolbar/settings/MapSettings.tsx | 14 +- .../ui/toolbar/settings/PluginSettings.tsx | 10 +- 19 files changed, 240 insertions(+), 212 deletions(-) create mode 100644 docs/src/plugin/export-livesplit.md diff --git a/compiler-core/src/expo/mod.rs b/compiler-core/src/expo/mod.rs index fb7901cd..21e5ed95 100644 --- a/compiler-core/src/expo/mod.rs +++ b/compiler-core/src/expo/mod.rs @@ -10,8 +10,8 @@ //! The output is a [`ExpoContext`]. use serde_json::Value; -use crate::exec::ExecContext; use crate::comp::CompDoc; +use crate::exec::ExecContext; use crate::macros::derive_wasm; /// Output of the export phase @@ -134,9 +134,9 @@ impl<'p> CompDoc<'p> { 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())) }, + Ok(None) => None, + Ok(Some(expo_doc)) => Some(expo_doc), + Err(e) => Some(ExpoDoc::Error(e.to_string())), }; self.plugin_runtimes = plugins; return result; @@ -144,7 +144,10 @@ impl<'p> CompDoc<'p> { } self.plugin_runtimes = plugins; - Some(ExpoDoc::Error(format!("Plugin {} not found", req.plugin_id))) + Some(ExpoDoc::Error(format!( + "Plugin {} not found", + req.plugin_id + ))) } } @@ -155,10 +158,11 @@ impl<'p> ExecContext<'p> { 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()) }, - }; + 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; } } diff --git a/compiler-core/src/prep/mod.rs b/compiler-core/src/prep/mod.rs index 8d3312eb..d8a1e2c2 100644 --- a/compiler-core/src/prep/mod.rs +++ b/compiler-core/src/prep/mod.rs @@ -272,7 +272,6 @@ where } }; - if let Some(redirect_path) = path { return match Use::new(redirect_path.clone()) { Use::Valid(valid) if matches!(valid, ValidUse::Absolute(_)) => { @@ -293,7 +292,7 @@ where "unreachable".to_string(), )) } - } + }; } } diff --git a/compiler-wasm/src/compiler/cache.rs b/compiler-wasm/src/compiler/cache.rs index fb711957..92bf347c 100644 --- a/compiler-wasm/src/compiler/cache.rs +++ b/compiler-wasm/src/compiler/cache.rs @@ -1,20 +1,10 @@ - use std::cell::RefCell; -use std::ops::{Deref, DerefMut}; -use celerc::lang::DocDiagnostic; -use instant::Instant; -use log::{error, info}; -use wasm_bindgen::prelude::*; +use log::info; -use celerc::pack::PackError; -use celerc::{ - CompDoc, CompileContext, Compiler, ContextBuilder, ExecContext, PluginOptions, PreparedContext, -}; +use celerc::PreparedContext; -use crate::interop::OpaqueExpoContext; use crate::loader::{self, LoadFileOutput, LoaderInWasm}; -use crate::plugin; thread_local! { static CACHED_COMPILER_CONTEXT: RefCell>> = RefCell::new(None); @@ -50,7 +40,7 @@ impl CachedContextGuard { } } Some(CachedContextGuard(Some(prep_ctx))) - }, + } None => None, } } @@ -94,4 +84,3 @@ impl AsRef> for CachedContextGuard { // return false; // } // } - diff --git a/compiler-wasm/src/compiler/export.rs b/compiler-wasm/src/compiler/export.rs index 2c433737..53037ef0 100644 --- a/compiler-wasm/src/compiler/export.rs +++ b/compiler-wasm/src/compiler/export.rs @@ -1,12 +1,12 @@ use instant::Instant; -use wasm_bindgen::prelude::*; use log::{error, info}; +use wasm_bindgen::prelude::*; -use celerc::{ExpoDoc, ExportRequest, PreparedContext, PluginOptions, Compiler}; use celerc::pack::PackError; +use celerc::{Compiler, ExpoDoc, ExportRequest, PluginOptions, PreparedContext}; -use crate::plugin; use crate::loader::LoaderInWasm; +use crate::plugin; use super::CachedContextGuard; @@ -66,7 +66,10 @@ fn export_with_pack_error(error: PackError) -> Result { Ok(ExpoDoc::Error(error.to_string())) } -async fn export_with_compiler(compiler: Compiler<'_>, req: ExportRequest) -> Result { +async fn export_with_compiler( + compiler: Compiler<'_>, + req: ExportRequest, +) -> Result { let mut comp_doc = compiler.compile().await; if let Some(expo_doc) = comp_doc.run_exporter(&req) { return Ok(expo_doc); diff --git a/compiler-wasm/src/compiler/mod.rs b/compiler-wasm/src/compiler/mod.rs index 1c9e4533..311bf655 100644 --- a/compiler-wasm/src/compiler/mod.rs +++ b/compiler-wasm/src/compiler/mod.rs @@ -1,6 +1,3 @@ -use std::cell::RefCell; -use std::ops::{Deref, DerefMut}; - use celerc::lang::DocDiagnostic; use celerc::prep::PrepResult; use instant::Instant; @@ -13,7 +10,7 @@ use celerc::{ }; use crate::interop::OpaqueExpoContext; -use crate::loader::{self, LoadFileOutput, LoaderInWasm}; +use crate::loader::LoaderInWasm; use crate::plugin; mod cache; @@ -45,20 +42,6 @@ pub async fn compile_document( } } - - - // if use_cache && is_cached_compiler_valid(entry_path.as_ref()).await { - // let cached_context = CACHED_COMPILER_CONTEXT.with_borrow_mut(|x| x.take()); - // - // if let Some(prep_ctx) = cached_context { - // info!("using cached compiler context"); - // let start_time = Instant::now(); - // let result = compile_in_context(&prep_ctx, Some(start_time), plugin_options).await; - // CACHED_COMPILER_CONTEXT.with_borrow_mut(|x| x.replace(prep_ctx)); - // return result; - // } - // } - // create a new context info!("creating new compiler context"); let start_time = Instant::now(); @@ -84,34 +67,6 @@ pub async fn new_context(entry_path: Option) -> PrepResult) -> bool { -// // TODO #173: better cache invalidation when local config changes -// -// let root_project_result = loader::load_file_check_changed("project.yaml").await; -// if !matches!(root_project_result, Ok(LoadFileOutput::NotModified)) { -// info!("root project.yaml is modified"); -// return false; -// } -// if let Some(entry_path) = entry_path { -// let entry_path = match entry_path.strip_prefix('/') { -// Some(x) => x, -// None => entry_path, -// }; -// let entry_result = loader::load_file_check_changed(entry_path).await; -// if !matches!(entry_result, Ok(LoadFileOutput::NotModified)) { -// info!("entry project.yaml is modified"); -// return false; -// } -// } -// let is_same = CACHED_COMPILER_ENTRY_PATH.with_borrow(|x| x.as_ref() == entry_path); -// if !is_same { -// info!("entry changed"); -// return false; -// } -// -// true -// } - async fn compile_in_context( prep_ctx: &PreparedContext, start_time: Option, diff --git a/compiler-wasm/src/lib.rs b/compiler-wasm/src/lib.rs index b0cc8e47..2407d352 100644 --- a/compiler-wasm/src/lib.rs +++ b/compiler-wasm/src/lib.rs @@ -1,4 +1,4 @@ -use celerc::{PluginOptionsRaw, ExportRequest, ExpoDoc}; +use celerc::{ExpoDoc, ExportRequest, PluginOptionsRaw}; use js_sys::Function; use log::info; use wasm_bindgen::prelude::*; @@ -63,7 +63,7 @@ pub async fn compile_document( pub async fn export_document( entry_path: Option, use_cache: bool, - req: ExportRequest + req: ExportRequest, ) -> Result { compiler::export_document(entry_path, use_cache, req).await } diff --git a/docs/src/plugin/built-in.md b/docs/src/plugin/built-in.md index bc6267b6..1fc2f2ba 100644 --- a/docs/src/plugin/built-in.md +++ b/docs/src/plugin/built-in.md @@ -4,6 +4,7 @@ Here is a list of all built-in plugins. The `ID` column is what you put after `u |-|-|-| |[Link](./link.md)|`link`|Turns `link` tags into clickable links| |[Variables](./variables.md)|`variables`|Adds a variable system that can be used to track completion, item counts, etc.| +|[Export LiveSplit](./export-livesplit.md)|`export-livesplit`|Export the route as LiveSplit splits| diff --git a/docs/src/plugin/export-livesplit.md b/docs/src/plugin/export-livesplit.md new file mode 100644 index 00000000..9145dfec --- /dev/null +++ b/docs/src/plugin/export-livesplit.md @@ -0,0 +1,2 @@ +# Export LiveSplit +TODO #33 diff --git a/web-client/src/core/doc/export.ts b/web-client/src/core/doc/export.ts index 0fd2ae0a..574206cc 100644 --- a/web-client/src/core/doc/export.ts +++ b/web-client/src/core/doc/export.ts @@ -1,40 +1,49 @@ import YAML from "js-yaml"; import { ExportMetadata, ExportRequest } from "low/celerc"; -import { DocSettingsState } from "./state"; import { Result, allocErr, allocOk } from "low/utils"; +import { DocSettingsState } from "./state"; + /// Get a unique identifier for the export metadata /// /// Used as the key in config storage export const getExporterId = (metadata: ExportMetadata): string => { - return `${metadata.pluginId}${metadata.pluginId.length}${metadata.exportId || ""}`; -} + return `${metadata.pluginId}${metadata.pluginId.length}${ + metadata.exportId || "" + }`; +}; /// Check if the export needs a config to be set /// /// An exporter must provide an example config string, if it wants to take config as input export const isConfigNeeded = (metadata: ExportMetadata): boolean => { return !!metadata.exampleConfig; -} +}; /// Get the config string for the export from settings, or use the default from the metadata -export const getExportConfig = (metadata: ExportMetadata, state: DocSettingsState): string => { +export const getExportConfig = ( + metadata: ExportMetadata, + state: DocSettingsState, +): string => { const id = getExporterId(metadata); if (id in state.exportConfigs) { return state.exportConfigs[id]; } return metadata.exampleConfig || ""; -} +}; /// Get the display label for the export, with the name and file extension export const getExportLabel = (metadata: ExportMetadata): string => { return metadata.extension ? `${metadata.name} (*.${metadata.extension})` : metadata.name; -} +}; -export const createExportRequest = (metadata: ExportMetadata, config: string): Result => { +export const createExportRequest = ( + metadata: ExportMetadata, + config: string, +): Result => { try { const configPayload = YAML.load(config); const request: ExportRequest = { @@ -46,4 +55,4 @@ export const createExportRequest = (metadata: ExportMetadata, config: string): R } catch (e) { return allocErr(e); } -} +}; diff --git a/web-client/src/core/doc/settingsReducers.ts b/web-client/src/core/doc/settingsReducers.ts index 907ec352..130aa6eb 100644 --- a/web-client/src/core/doc/settingsReducers.ts +++ b/web-client/src/core/doc/settingsReducers.ts @@ -92,15 +92,21 @@ export const setUserPluginConfig = withPayload( }, ); -export const setExportConfig = withPayload((state, {metadata, config}) => { +export const setExportConfig = withPayload< + DocSettingsState, + { + metadata: ExportMetadata; + config: string; + } +>((state, { metadata, config }) => { state.exportConfigs[getExporterId(metadata)] = config; }); -export const setExportConfigToDefault = withPayload((state, {metadata}) => { +export const setExportConfigToDefault = withPayload< + DocSettingsState, + { + metadata: ExportMetadata; + } +>((state, { metadata }) => { delete state.exportConfigs[getExporterId(metadata)]; }); diff --git a/web-client/src/core/kernel/AlertMgr.ts b/web-client/src/core/kernel/AlertMgr.ts index d2af53cf..02eb0526 100644 --- a/web-client/src/core/kernel/AlertMgr.ts +++ b/web-client/src/core/kernel/AlertMgr.ts @@ -42,7 +42,7 @@ export type BlockingAlertOptions = { component: React.ComponentType; /// Text for the cancel button. Default is "Cancel" cancelButton?: string; -} +}; type IdsOf = T[number]["id"]; type AlertCallback = (ok: boolean | string) => void; @@ -118,11 +118,10 @@ export class AlertMgr { /// cancels. /// /// If f throws, the alert will be cleared, and Err(e) will be returned. - public showBlocking({ - title, - component, - cancelButton, - }: BlockingAlertOptions, f: () => Promise): Promise> { + public showBlocking( + { title, component, cancelButton }: BlockingAlertOptions, + f: () => Promise, + ): Promise> { return new Promise((resolve) => { let cancelled = false; this.initAlert(() => { @@ -143,11 +142,15 @@ export class AlertMgr { ); // let the UI update first setTimeout(() => { - f().then((result) => { - if (!cancelled) { - this.clearAlertAndThen(() => resolve(allocOk(result))); - } - }).catch(e => { + f() + .then((result) => { + if (!cancelled) { + this.clearAlertAndThen(() => + resolve(allocOk(result)), + ); + } + }) + .catch((e) => { if (!cancelled) { this.clearAlertAndThen(() => resolve(allocErr(e))); } @@ -184,8 +187,8 @@ export class AlertMgr { const element = this.previousFocusedElement; if ( element && - "focus" in element && - typeof element.focus === "function" + "focus" in element && + typeof element.focus === "function" ) { element.focus(); } diff --git a/web-client/src/core/kernel/Kernel.ts b/web-client/src/core/kernel/Kernel.ts index c7d5a8ec..f54c2554 100644 --- a/web-client/src/core/kernel/Kernel.ts +++ b/web-client/src/core/kernel/Kernel.ts @@ -13,6 +13,7 @@ import { viewActions, } from "core/store"; import { isRecompileNeeded } from "core/doc"; +import { ExpoDoc, ExportRequest } from "low/celerc"; import { console, Logger, isInDarkMode } from "low/utils"; import type { FileSys, FsResult } from "low/fs"; @@ -21,7 +22,6 @@ import type { EditorKernel } from "./editor"; import { KeyMgr } from "./KeyMgr"; import { WindowMgr } from "./WindowMgr"; import { AlertMgr } from "./AlertMgr"; -import { ExpoDoc, ExportRequest } from "low/celerc"; type InitUiFunction = ( kernel: Kernel, @@ -196,7 +196,9 @@ export class Kernel { const state = this.store.getState(); const stageMode = viewSelector(state).stageMode; if (stageMode !== "edit") { - this.log.error("compiler is not available in view mode. This is a bug!"); + this.log.error( + "compiler is not available in view mode. This is a bug!", + ); throw new Error("compiler is not available in view mode"); } if (!this.compiler) { @@ -357,7 +359,7 @@ export class Kernel { // TODO #184: export from server return { error: "Export from server is not available yet. This is tracked by issue 184 on GitHub", - } + }; } } } diff --git a/web-client/src/core/kernel/compiler/CompilerKernelImpl.ts b/web-client/src/core/kernel/compiler/CompilerKernelImpl.ts index 9ef02c9d..e1000075 100644 --- a/web-client/src/core/kernel/compiler/CompilerKernelImpl.ts +++ b/web-client/src/core/kernel/compiler/CompilerKernelImpl.ts @@ -222,26 +222,28 @@ export class CompilerKernelImpl implements CompilerKernel { public async export(request: ExportRequest): Promise { if (!this.fileAccess) { return { - error: "Compiler not available. Please make sure a project is loaded." + error: "Compiler not available. Please make sure a project is loaded.", }; } if (!(await this.ensureReady())) { return { - error: "Compiler is not ready. Please try again later." + error: "Compiler is not ready. Please try again later.", }; } const validatedEntryPathResult = await this.validateEntryPath(); if (validatedEntryPathResult.isErr()) { return { - error: "Compiler entry path is invalid. Please check your settings." + error: "Compiler entry path is invalid. Please check your settings.", }; } const validatedEntryPath = validatedEntryPathResult.inner(); return await this.compilerLock.lockedScope(undefined, async () => { - const { compilerUseCachedPrepPhase } = settingsSelector(this.store.getState()); + const { compilerUseCachedPrepPhase } = settingsSelector( + this.store.getState(), + ); await this.updatePluginOptions(); @@ -259,7 +261,6 @@ export class CompilerKernelImpl implements CompilerKernel { } return result.inner(); }); - } /// Try to wait for the compiler to be ready. Returns true if it becomes ready eventually. @@ -383,7 +384,9 @@ export class CompilerKernelImpl implements CompilerKernel { }); if (result.isErr()) { CompilerLog.error(result.inner()); - CompilerLog.warn("failed to set plugin options. The output may be wrong."); + CompilerLog.warn( + "failed to set plugin options. The output may be wrong.", + ); } else { CompilerLog.info("plugin options updated"); } diff --git a/web-client/src/low/utils/FileSaver/index.ts b/web-client/src/low/utils/FileSaver/index.ts index e84be20a..9e73173a 100644 --- a/web-client/src/low/utils/FileSaver/index.ts +++ b/web-client/src/low/utils/FileSaver/index.ts @@ -1,7 +1,10 @@ // @ts-expect-error no types for this library import FileSaverFunction from "./FileSaver"; -export const saveAs = (content: string | Uint8Array, filename: string): void => { +export const saveAs = ( + content: string | Uint8Array, + filename: string, +): void => { const blob = new Blob([content], { // maybe lying, but should be fine type: "text/plain;charset=utf-8", diff --git a/web-client/src/low/utils/WorkerHost.ts b/web-client/src/low/utils/WorkerHost.ts index d2f41bf9..440179f1 100644 --- a/web-client/src/low/utils/WorkerHost.ts +++ b/web-client/src/low/utils/WorkerHost.ts @@ -40,7 +40,9 @@ export function setWorker(w: Worker, logger: Logger) { // Event handler const handler = workerHandlers[handleId]; if (!handler) { - console.warn(`no worker handler for handleId=${handleId}. This could possibly be due to a previous panic from the worker.`); + console.warn( + `no worker handler for handleId=${handleId}. This could possibly be due to a previous panic from the worker.`, + ); return; } const [resolve, reject, timeoutHandle] = handler; diff --git a/web-client/src/ui/shared/PrismEditor/PrismEditorCore.tsx b/web-client/src/ui/shared/PrismEditor/PrismEditorCore.tsx index 3279560b..11899722 100644 --- a/web-client/src/ui/shared/PrismEditor/PrismEditorCore.tsx +++ b/web-client/src/ui/shared/PrismEditor/PrismEditorCore.tsx @@ -1,4 +1,8 @@ -import { makeStyles, mergeClasses, shorthands } from "@fluentui/react-components"; +import { + makeStyles, + mergeClasses, + shorthands, +} from "@fluentui/react-components"; import ReactSimpleCodeEditor from "react-simple-code-editor"; import { highlight, languages } from "prismjs"; @@ -48,7 +52,7 @@ function initStyles() { }, outerDisabled: { backgroundColor: dark ? "#333" : "#ddd", - } + }, }); } const useStyles = initStyles(); @@ -61,7 +65,12 @@ const PrismEditorCore: React.FC = ({ }) => { const styles = useStyles(); return ( -
+
((_, ref) => { const { enabled, tooltip, exportMetadata } = useExportControl(); @@ -108,7 +119,11 @@ const ExportInternal: React.FC = ({ {exportMetadata?.map((_, i) => ( - + ))} @@ -116,7 +131,10 @@ const ExportInternal: React.FC = ({ ); }; -const ExportButton: React.FC<{ exportMetadata: ExportMetadata[], index: number }> = ({ exportMetadata, index }) => { +const ExportButton: React.FC<{ + exportMetadata: ExportMetadata[]; + index: number; +}> = ({ exportMetadata, index }) => { const metadata = exportMetadata[index]; const text = getExportLabel(metadata); @@ -128,9 +146,11 @@ const ExportButton: React.FC<{ exportMetadata: ExportMetadata[], index: number } relationship="label" positioning="after" > - } - onClick={() => runExportWizard(exportMetadata, index, kernel, store)} + onClick={() => + runExportWizard(exportMetadata, index, kernel, store) + } > {text} @@ -163,15 +183,24 @@ const ExportIconComponent: React.FC<{ name: ExportIcon | undefined }> = ({ } }; -const runExportWizard = async (exportMetadata: ExportMetadata[], index: number, kernel: Kernel, store: AppStore) => { +const runExportWizard = async ( + exportMetadata: ExportMetadata[], + index: number, + kernel: Kernel, + store: AppStore, +) => { const state = store.getState(); let selection = index; let error: string | undefined = undefined; // show the extra config dialog based on the initial selection const enableConfig = isConfigNeeded(exportMetadata[selection]); - let config: string = getExportConfig(exportMetadata[selection], settingsSelector(state)); + let config: string = getExportConfig( + exportMetadata[selection], + settingsSelector(state), + ); // eslint-disable-next-line no-constant-condition - while(true) { + while (true) { + // show extra config dialog if needed if (enableConfig) { const ok = await kernel.getAlertMgr().showRich({ title: "Export", @@ -198,37 +227,25 @@ const runExportWizard = async (exportMetadata: ExportMetadata[], index: number, return; } } - store.dispatch(settingsActions.setExportConfig({metadata: exportMetadata[selection], config})); - const result = await kernel.getAlertMgr().showBlocking({ - title: "Export", - component: () => { - return ( - <> - - Generating the export file... Download will automatically start once done. - -
- -
- - ); - } - }, async (): Promise => { - const requestResult = createExportRequest(exportMetadata[selection], config); - if (requestResult.isErr()) { - return {error: errorToString(requestResult.inner()) }; - } - const request = requestResult.inner(); - return await kernel.export(request); - }); + // persist the config to settings + store.dispatch( + settingsActions.setExportConfig({ + metadata: exportMetadata[selection], + config, + }), + ); + const result = await runExportAndShowDialog( + kernel, + exportMetadata[selection], + config, + ); if (result.isOk()) { const expoDoc = result.inner(); if ("success" in expoDoc) { - const file = expoDoc.success; - // todo: download file - console.log(file); + // const _file = expoDoc.success; + // TODO #33: download file return; - } + } error = expoDoc.error; } else { @@ -240,7 +257,7 @@ const runExportWizard = async (exportMetadata: ExportMetadata[], index: number, error = errorToString(v); } } -} +}; type ExportDialogProps = { /// All available export options @@ -253,7 +270,7 @@ type ExportDialogProps = { /// The config string config: string; onConfigChange: (config: string) => void; -} +}; const ExportDialog: React.FC = ({ exportMetadata, initialSelectionIndex, @@ -264,7 +281,6 @@ const ExportDialog: React.FC = ({ }) => { const { setExportConfigToDefault } = useActions(settingsActions); - const [selectedIndex, setSelectedIndex] = useState(initialSelectionIndex); const [configValue, setConfigValue] = useState(config); @@ -273,10 +289,7 @@ const ExportDialog: React.FC = ({ return ( <> - + = ({ onSelectionChange(index); }} > - { - exportMetadata.map((metadata, i) => ( - - )) - } + {exportMetadata.map((metadata, i) => ( + + ))} - {enableConfig && + {enableConfig && ( <> - - + + This export option accepts extra configuration. - {metadata.learnMore && + {metadata.learnMore && ( <> {" "} - + Learn more - { - !(metadata.learnMore.startsWith("/") || metadata.learnMore.startsWith(window.location.origin)) && " (external link)" - } + {!( + metadata.learnMore.startsWith("/") || + metadata.learnMore.startsWith( + window.location.origin, + ) + ) && " (external link)"} - } + )} - { error && - - {error} - - } + {error && {error}} = ({ onConfigChange(x); }} /> - - } + )} ); -} +}; + +/// Run the export and show a blocking dialog +const runExportAndShowDialog = async ( + kernel: Kernel, + metadata: ExportMetadata, + config: string, +) => { + const result = await kernel.getAlertMgr().showBlocking( + { + title: "Export", + component: () => { + return ( + <> + + Generating the export file... Download will + automatically start once done. + +
+ +
+ + ); + }, + }, + async (): Promise => { + const requestResult = createExportRequest(metadata, config); + if (requestResult.isErr()) { + return { error: errorToString(requestResult.inner()) }; + } + const request = requestResult.inner(); + return await kernel.export(request); + }, + ); + return result; +}; diff --git a/web-client/src/ui/toolbar/settings/MapSettings.tsx b/web-client/src/ui/toolbar/settings/MapSettings.tsx index 4ea5fe81..c075de55 100644 --- a/web-client/src/ui/toolbar/settings/MapSettings.tsx +++ b/web-client/src/ui/toolbar/settings/MapSettings.tsx @@ -235,16 +235,16 @@ const SectionModeSelector: React.FC = ({ setValue(data.selectedOptions[0] as SectionMode); }} > - - - - @@ -289,13 +289,13 @@ const LayerModeSelector: React.FC = ({ setValue(data.selectedOptions[0] as LayerMode); }} > - - - diff --git a/web-client/src/ui/toolbar/settings/PluginSettings.tsx b/web-client/src/ui/toolbar/settings/PluginSettings.tsx index 74506e2f..52151d6f 100644 --- a/web-client/src/ui/toolbar/settings/PluginSettings.tsx +++ b/web-client/src/ui/toolbar/settings/PluginSettings.tsx @@ -100,10 +100,7 @@ export const PluginSettings: React.FC = () => { <> Configure extra plugins to use when loading route documents.{" "} - + Learn more @@ -247,10 +244,7 @@ const UserPluginConfigEditor: React.FC = ({
Please edit your plugin configuration below.{" "} - + Learn more From b00a33b5079a8114cdc3c1b5760e689c5c3cea08 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Sat, 10 Feb 2024 06:09:07 -0800 Subject: [PATCH 4/5] clean up comments --- compiler-wasm/src/compiler/cache.rs | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/compiler-wasm/src/compiler/cache.rs b/compiler-wasm/src/compiler/cache.rs index 92bf347c..a8677a2f 100644 --- a/compiler-wasm/src/compiler/cache.rs +++ b/compiler-wasm/src/compiler/cache.rs @@ -60,27 +60,3 @@ impl AsRef> for CachedContextGuard { self.0.as_ref().unwrap() } } - -// pub async fn is_cache_valid(entry_path: Option<&String>) -> bool { -// let root_project_result = loader::load_file_check_changed("project.yaml").await; -// if !matches!(root_project_result, Ok(LoadFileOutput::NotModified)) { -// info!("root project.yaml is modified"); -// return false; -// } -// if let Some(entry_path) = entry_path { -// let entry_path = match entry_path.strip_prefix('/') { -// Some(x) => x, -// None => entry_path, -// }; -// let entry_result = loader::load_file_check_changed(entry_path).await; -// if !matches!(entry_result, Ok(LoadFileOutput::NotModified)) { -// info!("entry project.yaml is modified"); -// return false; -// } -// } -// let is_same = CACHED_COMPILER_ENTRY_PATH.with_borrow(|x| x.as_ref() == entry_path); -// if !is_same { -// info!("entry changed"); -// return false; -// } -// } From e00f01f9b15fdadbb4e86b8c17650f0c02b3df8b Mon Sep 17 00:00:00 2001 From: Pistonight Date: Sat, 10 Feb 2024 06:15:23 -0800 Subject: [PATCH 5/5] upgrade rust to 1.76 --- compiler-core/src/comp/comp_section.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler-core/src/comp/comp_section.rs b/compiler-core/src/comp/comp_section.rs index 6e32d5a2..f470eec8 100644 --- a/compiler-core/src/comp/comp_section.rs +++ b/compiler-core/src/comp/comp_section.rs @@ -47,7 +47,7 @@ impl<'p> Compiler<'p> { pub async fn compile_section( &self, value: RouteBlobRef<'p>, - route: &Vec, + route: &[CompSection], ) -> Option { let result = match value.try_as_single_key_object() { RouteBlobSingleKeyObjectResult::Ok(key, value) => Ok((key, value)),