Compare commits

...

13 Commits

Author SHA1 Message Date
aa52407fda Cut release v0.19.3 (#2251) 2024-04-25 13:28:42 -07:00
e45be831d0 Pass the ?pool query param through to the backend. (#2246)
Pass the ?pool query param through to the backend.

This will slice off the ?pool= param and pass it to the WebSocket
request, which requests that the Zoo API use a particular pool of
engines. This isn't something any users of the zoo api require; but it's
needed for the internal engine Zoo development workflow. This may be
used in the future, but for now this'll be always enabled. Passing any
value in the production servers will result in a "no backend" error for
now.
2024-04-25 19:51:33 +00:00
005944f3a3 fix the updater (#2250)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 12:41:22 -07:00
755ef8ce7f download-wasm if there's no rust changes (#2234)
* download-wasm if there's no rust changes

* typo

* typo

* artifact stuff

* add needs

* permissions

* hmm

* more logic

* same for ubuntu
2024-04-26 05:37:32 +10:00
005d1f0ca7 Filter files and folders that start with a . (#2249) 2024-04-25 19:01:50 +00:00
e158f6f513 Better rust parsing of route uris for files (#2248)
* refactors

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fiex;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fiex;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 18:55:11 +00:00
879d7ec4f4 Cut release v0.19.2 (#2247) 2024-04-25 14:38:25 -04:00
f6838b9b14 always ensure the dirs exist (#2245)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 17:07:24 +00:00
cb75c47631 fix env vars for lsp server to match other .env vars (#2243)
fix env vars for lsp

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 16:41:39 +00:00
9b95ec1083 fix relevant extensions (#2241)
* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 08:36:45 -07:00
a3eeff65c8 Cut release v0.19.1 (#2240) 2024-04-25 10:40:31 -04:00
fab3d2b130 Make FileTree a pane (desktop only) (#2232) 2024-04-25 09:56:55 -04:00
0a96dc6fd2 Project state improvements (#2239) 2024-04-25 12:52:08 +00:00
37 changed files with 1089 additions and 229 deletions

View File

@ -16,8 +16,6 @@ jobs:
cache: 'yarn'
- name: Install dependencies
run: yarn
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache wasm

View File

@ -12,11 +12,31 @@ concurrency:
permissions:
contents: write
pull-requests: write
actions: read
jobs:
check-rust-changes:
runs-on: ubuntu-latest
outputs:
rust-changed: ${{ steps.filter.outputs.rust }}
steps:
- uses: actions/checkout@v4
- id: filter
name: Check for Rust changes
uses: dorny/paths-filter@v2
with:
filters: |
rust:
- 'src/wasm-lib/**'
playwright-ubuntu:
timeout-minutes: 60
runs-on: ubuntu-latest-8-cores
needs: check-rust-changes
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -28,13 +48,39 @@ jobs:
run: yarn
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: download wasm
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: actions/download-artifact@v2
continue-on-error: true
with:
name: wasm-bundle
path: src/wasm-lib/pkg
- name: copy wasm blob
if: needs.check-rust-changes.outputs.rust-changed == 'false'
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
continue-on-error: true
- name: Setup Rust
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust
if: steps.download-wasm.outcome == 'failure'
uses: dtolnay/rust-toolchain@stable
- name: Cache wasm
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Cache wasm
if: steps.download-wasm.outcome == 'failure'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: build wasm
if: needs.check-rust-changes.outputs.rust-changed == 'true'
run: yarn build:wasm
- name: build wasm
if: steps.download-wasm.outcome == 'failure'
run: yarn build:wasm
- name: build web
run: yarn build:local
@ -85,10 +131,16 @@ jobs:
name: playwright-report
path: playwright-report/
retention-days: 30
- uses: actions/upload-artifact@v4
if: github.ref == 'refs/heads/main'
with:
name: wasm-bundle
path: src/wasm-lib/pkg
playwright-macos:
timeout-minutes: 60
runs-on: macos-14
needs: check-rust-changes
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -99,13 +151,39 @@ jobs:
run: yarn
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: download wasm
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: actions/download-artifact@v4
with:
name: wasm-bundle
path: src/wasm-lib/pkg
continue-on-error: true
- name: copy wasm blob
if: needs.check-rust-changes.outputs.rust-changed == 'false'
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
continue-on-error: true
- name: Setup Rust
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust
if: steps.download-wasm.outcome == 'failure'
uses: dtolnay/rust-toolchain@stable
- name: Cache wasm
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Cache wasm
if: steps.download-wasm.outcome == 'failure'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: build wasm
if: needs.check-rust-changes.outputs.rust-changed == 'true'
run: yarn build:wasm
- name: build wasm
if: steps.download-wasm.outcome == 'failure'
run: yarn build:wasm
- name: build web
run: yarn build:local
@ -122,7 +200,7 @@ jobs:
name: playwright-report
path: playwright-report/
retention-days: 30
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v4
if: github.ref == 'refs/heads/main'
with:
name: wasm-bundle

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.19.0",
"version": "0.19.3",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.16.0",

136
src-tauri/Cargo.lock generated
View File

@ -90,6 +90,54 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]]
name = "anyhow"
version = "1.0.82"
@ -110,6 +158,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-cli",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-http",
@ -695,6 +744,48 @@ dependencies = [
"inout",
]
[[package]]
name = "clap"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim 0.11.1",
"unicase",
"unicode-width",
]
[[package]]
name = "clap_derive"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.60",
]
[[package]]
name = "clap_lex"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "cocoa"
version = "0.25.0"
@ -725,6 +816,12 @@ dependencies = [
"objc",
]
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "colored"
version = "2.1.0"
@ -915,7 +1012,7 @@ dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"strsim 0.10.0",
"syn 2.0.60",
]
@ -2321,7 +2418,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.1.51"
version = "0.1.53"
dependencies = [
"anyhow",
"approx",
@ -2330,6 +2427,7 @@ dependencies = [
"base64 0.22.0",
"bson",
"chrono",
"clap",
"dashmap",
"databake",
"derive-docs",
@ -2389,6 +2487,7 @@ dependencies = [
"bigdecimal",
"bytes",
"chrono",
"clap",
"data-encoding",
"format_serde_error",
"futures",
@ -4580,6 +4679,12 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "structmeta"
version = "0.2.0"
@ -4958,6 +5063,21 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-cli"
version = "2.0.0-beta.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b079f01e923f7d3bf175e8d31b18861e6580f4b57ce0fdc16fbf69f9acd158c"
dependencies = [
"clap",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.0.0-beta.6"
@ -5783,6 +5903,12 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "untrusted"
version = "0.9.0"
@ -5820,6 +5946,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.8.0"

View File

@ -15,11 +15,12 @@ tauri-build = { version = "2.0.0-beta.13", features = [] }
[dependencies]
anyhow = "1"
kcl-lib = { version = "0.1.51", path = "../src/wasm-lib/kcl" }
kcl-lib = { version = "0.1.53", path = "../src/wasm-lib/kcl" }
kittycad = "0.3.0"
oauth2 = "4.4.2"
serde_json = "1.0"
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
tauri-plugin-cli = { version = "2.0.0-beta.3" }
tauri-plugin-dialog = { version = "2.0.0-beta.6" }
tauri-plugin-fs = { version = "2.0.0-beta.6" }
tauri-plugin-http = { version = "2.0.0-beta.6" }

View File

@ -7,6 +7,7 @@
"main"
],
"permissions": [
"cli:default",
"path:default",
"event:default",
"window:default",
@ -23,7 +24,6 @@
"fs:allow-copy-file",
"fs:allow-mkdir",
"fs:allow-remove",
"fs:allow-remove",
"fs:allow-rename",
"fs:allow-exists",
"fs:allow-stat",

View File

@ -1,6 +1,8 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
pub(crate) mod state;
use std::{
env,
path::{Path, PathBuf},
@ -9,12 +11,13 @@ use std::{
use anyhow::Result;
use kcl_lib::settings::types::{
file::{FileEntry, Project},
file::{FileEntry, Project, ProjectRoute, ProjectState},
project::ProjectConfiguration,
Configuration,
Configuration, DEFAULT_PROJECT_KCL_FILE,
};
use oauth2::TokenResponse;
use tauri::{ipc::InvokeError, Manager};
use tauri_plugin_cli::CliExt;
use tauri_plugin_shell::ShellExt;
const DEFAULT_HOST: &str = "https://api.zoo.dev";
@ -36,14 +39,35 @@ fn get_initial_default_dir(app: tauri::AppHandle) -> Result<PathBuf, InvokeError
Ok(dir.join(PROJECT_FOLDER))
}
fn get_app_settings_file_path(app: &tauri::AppHandle) -> Result<PathBuf, InvokeError> {
#[tauri::command]
async fn get_state(app: tauri::AppHandle) -> Result<Option<ProjectState>, InvokeError> {
let store = app.state::<state::Store>();
Ok(store.get().await)
}
#[tauri::command]
async fn set_state(app: tauri::AppHandle, state: Option<ProjectState>) -> Result<(), InvokeError> {
let store = app.state::<state::Store>();
store.set(state).await;
Ok(())
}
async fn get_app_settings_file_path(app: &tauri::AppHandle) -> Result<PathBuf, InvokeError> {
let app_config_dir = app.path().app_config_dir()?;
// Ensure this directory exists.
if !app_config_dir.exists() {
tokio::fs::create_dir_all(&app_config_dir)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(app_config_dir.join(SETTINGS_FILE_NAME))
}
#[tauri::command]
async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration, InvokeError> {
let mut settings_path = get_app_settings_file_path(&app)?;
let mut settings_path = get_app_settings_file_path(&app).await?;
let mut needs_migration = false;
// Check if this file exists.
@ -88,7 +112,7 @@ async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration,
#[tauri::command]
async fn write_app_settings_file(app: tauri::AppHandle, configuration: Configuration) -> Result<(), InvokeError> {
let settings_path = get_app_settings_file_path(&app)?;
let settings_path = get_app_settings_file_path(&app).await?;
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
tokio::fs::write(settings_path, contents.as_bytes())
.await
@ -97,13 +121,19 @@ async fn write_app_settings_file(app: tauri::AppHandle, configuration: Configura
Ok(())
}
fn get_project_settings_file_path(app_settings: Configuration, project_name: &str) -> Result<PathBuf, InvokeError> {
Ok(app_settings
.settings
.project
.directory
.join(project_name)
.join(PROJECT_SETTINGS_FILE_NAME))
async fn get_project_settings_file_path(
app_settings: Configuration,
project_name: &str,
) -> Result<PathBuf, InvokeError> {
let project_dir = app_settings.settings.project.directory.join(project_name);
if !project_dir.exists() {
tokio::fs::create_dir_all(&project_dir)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(project_dir.join(PROJECT_SETTINGS_FILE_NAME))
}
#[tauri::command]
@ -111,7 +141,7 @@ async fn read_project_settings_file(
app_settings: Configuration,
project_name: &str,
) -> Result<ProjectConfiguration, InvokeError> {
let settings_path = get_project_settings_file_path(app_settings, project_name)?;
let settings_path = get_project_settings_file_path(app_settings, project_name).await?;
// Check if this file exists.
if !settings_path.exists() {
@ -133,7 +163,7 @@ async fn write_project_settings_file(
project_name: &str,
configuration: ProjectConfiguration,
) -> Result<(), InvokeError> {
let settings_path = get_project_settings_file_path(app_settings, project_name)?;
let settings_path = get_project_settings_file_path(app_settings, project_name).await?;
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
tokio::fs::write(settings_path, contents.as_bytes())
.await
@ -172,13 +202,19 @@ async fn list_projects(configuration: Configuration) -> Result<Vec<Project>, Inv
/// Get information about a project.
#[tauri::command]
async fn get_project_info(configuration: Configuration, project_name: &str) -> Result<Project, InvokeError> {
async fn get_project_info(configuration: Configuration, project_path: &str) -> Result<Project, InvokeError> {
configuration
.get_project_info(project_name)
.get_project_info(project_path)
.await
.map_err(InvokeError::from_anyhow)
}
/// Parse the project route.
#[tauri::command]
async fn parse_project_route(configuration: Configuration, route: &str) -> Result<ProjectRoute, InvokeError> {
ProjectRoute::from_route(&configuration, route).map_err(InvokeError::from_anyhow)
}
#[tauri::command]
async fn read_dir_recursive(path: &str) -> Result<FileEntry, InvokeError> {
kcl_lib::settings::utils::walk_dir(&Path::new(path).to_path_buf())
@ -316,23 +352,15 @@ fn show_in_folder(path: &str) -> Result<(), InvokeError> {
fn main() -> Result<()> {
tauri::Builder::default()
.setup(|_app| {
#[cfg(debug_assertions)]
{
_app.get_webview("main").unwrap().open_devtools();
}
#[cfg(not(debug_assertions))]
{
_app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
get_state,
set_state,
get_initial_default_dir,
initialize_project_directory,
create_new_project_directory,
list_projects,
get_project_info,
parse_project_route,
get_user,
login,
read_dir_recursive,
@ -342,6 +370,131 @@ fn main() -> Result<()> {
read_project_settings_file,
write_project_settings_file,
])
.plugin(tauri_plugin_cli::init())
.setup(|app| {
// Do update things.
#[cfg(debug_assertions)]
{
app.get_webview("main").unwrap().open_devtools();
}
#[cfg(not(debug_assertions))]
{
app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
}
let mut verbose = false;
let mut source_path: Option<PathBuf> = None;
match app.cli().matches() {
// `matches` here is a Struct with { args, subcommand }.
// `args` is `HashMap<String, ArgData>` where `ArgData` is a struct with { value, occurrences }.
// `subcommand` is `Option<Box<SubcommandMatches>>` where `SubcommandMatches` is a struct with { name, matches }.
Ok(matches) => {
if let Some(verbose_flag) = matches.args.get("verbose") {
let Some(value) = verbose_flag.value.as_bool() else {
return Err(
anyhow::anyhow!("Error parsing CLI arguments: verbose flag is not a boolean").into(),
);
};
verbose = value;
}
// Get the path we are trying to open.
if let Some(source_arg) = matches.args.get("source") {
// We don't do an else here because this can be null.
if let Some(value) = source_arg.value.as_str() {
source_path = Some(Path::new(value).to_path_buf());
}
}
}
Err(err) => {
return Err(anyhow::anyhow!("Error parsing CLI arguments: {:?}", err).into());
}
}
// If we have a source path to open, make sure it exists.
let Some(source_path) = source_path else {
// The user didn't provide a source path to open.
// Run the app as normal.
app.manage(state::Store::default());
return Ok(());
};
if !source_path.exists() {
return Err(anyhow::anyhow!(
"Error: the path `{}` you are trying to open does not exist",
source_path.display()
)
.into());
}
let runner: tauri::async_runtime::JoinHandle<Result<ProjectState>> =
tauri::async_runtime::spawn(async move {
// Fix for "." path, which is the current directory.
let source_path = if source_path == Path::new(".") {
std::env::current_dir()
.map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
} else {
source_path
};
// If the path is a directory, let's assume it is a project directory.
if source_path.is_dir() {
// Load the details about the project from the path.
let project = Project::from_path(&source_path).await.map_err(|e| {
anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e)
})?;
if verbose {
println!("Project loaded from path: {}", source_path.display());
}
// Create the default file in the project.
// Write the initial project file.
let project_file = source_path.join(DEFAULT_PROJECT_KCL_FILE);
tokio::fs::write(&project_file, vec![]).await?;
return Ok(ProjectState {
project,
current_file: Some(project_file.display().to_string()),
});
}
// We were given a file path, not a directory.
// Let's get the parent directory of the file.
let parent = source_path.parent().ok_or_else(|| {
anyhow::anyhow!(
"Error getting the parent directory of the file: {}",
source_path.display()
)
})?;
// Load the details about the project from the parent directory.
let project = Project::from_path(&parent).await.map_err(|e| {
anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e)
})?;
if verbose {
println!(
"Project loaded from path: {}, current file: {}",
parent.display(),
source_path.display()
);
}
Ok(ProjectState {
project,
current_file: Some(source_path.display().to_string()),
})
});
// Block on the handle.
let store = tauri::async_runtime::block_on(runner)??;
// Create a state object to hold the project.
app.manage(state::Store::new(store));
Ok(())
})
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_http::init())

21
src-tauri/src/state.rs Normal file
View File

@ -0,0 +1,21 @@
//! State management for the application.
use kcl_lib::settings::types::file::ProjectState;
use tokio::sync::Mutex;
#[derive(Debug, Default)]
pub struct Store(Mutex<Option<ProjectState>>);
impl Store {
pub fn new(p: ProjectState) -> Self {
Self(Mutex::new(Some(p)))
}
pub async fn get(&self) -> Option<ProjectState> {
self.0.lock().await.clone()
}
pub async fn set(&self, p: Option<ProjectState>) {
*self.0.lock().await = p;
}
}

View File

@ -50,10 +50,26 @@
},
"identifier": "dev.zoo.modeling-app",
"plugins": {
"cli": {
"description": "Zoo Modeling App CLI",
"args": [
{
"short": "v",
"name": "verbose",
"description": "Verbosity level"
},
{
"name": "source",
"index": 1,
"takesValue": true
}
],
"subcommands": {}
},
"shell": {
"open": true
}
},
"productName": "Zoo Modeling App",
"version": "0.19.0"
"version": "0.19.3"
}

View File

@ -30,6 +30,7 @@ import SettingsAuthProvider from 'components/SettingsAuthProvider'
import LspProvider from 'components/LspProvider'
import { KclContextProvider } from 'lang/KclProvider'
import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { getState, setState } from 'lib/tauri'
const router = createBrowserRouter([
{
@ -52,10 +53,29 @@ const router = createBrowserRouter([
children: [
{
path: paths.INDEX,
loader: () =>
isTauri()
loader: async () => {
const inTauri = isTauri()
if (inTauri) {
const appState = await getState()
if (appState) {
// Reset the state.
// We do this so that we load the initial state from the cli but everything
// else we can ignore.
await setState(undefined)
// Redirect to the file if we have a file path.
if (appState.current_file) {
return redirect(
paths.FILE + '/' + encodeURIComponent(appState.current_file)
)
}
}
}
return inTauri
? redirect(paths.HOME)
: redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME),
: redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME)
},
},
{
loader: fileLoader,

View File

@ -62,7 +62,7 @@ export const FileMachineProvider = ({
services: {
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
const newFiles = isTauri()
? (await getProjectInfo(context.project.name)).children
? (await getProjectInfo(context.project.path)).children
: []
return {
...context.project,

View File

@ -3,7 +3,7 @@ import { paths } from 'lib/paths'
import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip'
import { Dispatch, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { Dialog, Disclosure } from '@headlessui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
@ -133,18 +133,13 @@ const FileTreeItem = ({
project,
currentFile,
fileOrDir,
closePanel,
onDoubleClick,
level = 0,
}: {
project?: IndexLoaderData['project']
currentFile?: IndexLoaderData['file']
fileOrDir: FileEntry
closePanel: (
focusableElement?:
| HTMLElement
| React.MutableRefObject<HTMLElement | null>
| undefined
) => void
onDoubleClick?: () => void
level?: number
}) => {
const { send, context } = useFileContext()
@ -186,7 +181,7 @@ const FileTreeItem = ({
// Open kcl files
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
}
closePanel()
onDoubleClick?.()
}
return (
@ -194,8 +189,10 @@ const FileTreeItem = ({
{fileOrDir.children === undefined ? (
<li
className={
'group m-0 p-0 border-solid border-0 hover:text-primary hover:bg-primary/5 focus-within:bg-primary/5 ' +
(isCurrentFile ? '!bg-primary/10 !text-primary' : '')
'group m-0 p-0 border-solid border-0 hover:bg-primary/5 focus-within:bg-primary/5 dark:hover:bg-primary/20 dark:focus-within:bg-primary/20 ' +
(isCurrentFile
? '!bg-primary/10 !text-primary dark:!bg-primary/20 dark:!text-inherit'
: '')
}
>
{!isRenaming ? (
@ -227,9 +224,9 @@ const FileTreeItem = ({
{!isRenaming ? (
<Disclosure.Button
className={
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5' +
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5 dark:hover:text-inherit dark:hover:bg-primary/10' +
(context.selectedDirectory.path.includes(fileOrDir.path)
? ' ui-open:text-primary'
? ' ui-open:bg-primary/10'
: '')
}
style={{ paddingInlineStart: getIndentationCSS(level) }}
@ -293,7 +290,7 @@ const FileTreeItem = ({
fileOrDir={child}
project={project}
currentFile={currentFile}
closePanel={closePanel}
onDoubleClick={onDoubleClick}
level={level + 1}
key={level + '-' + child.path}
/>
@ -325,20 +322,8 @@ interface FileTreeProps {
) => void
}
export const FileTree = ({
className = '',
file,
closePanel,
}: FileTreeProps) => {
const { send, context } = useFileContext()
const docuemntHasFocus = useDocumentHasFocus()
useHotkeys('meta + n', createFile)
useHotkeys('meta + shift + n', createFolder)
// Refresh the file tree when the document gets focus
useEffect(() => {
send({ type: 'Refresh' })
}, [docuemntHasFocus])
export const FileTreeMenu = () => {
const { send } = useFileContext()
async function createFile() {
send({ type: 'Create file', data: { name: '', makeDir: false } })
@ -348,58 +333,88 @@ export const FileTree = ({
send({ type: 'Create file', data: { name: '', makeDir: true } })
}
useHotkeys('meta + n', createFile)
useHotkeys('meta + shift + n', createFolder)
return (
<>
<ActionButton
Element="button"
icon={{
icon: 'filePlus',
iconClassName: '!text-current',
bgClassName: 'bg-transparent',
}}
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
onClick={createFile}
>
<Tooltip position="bottom-right" delay={750}>
Create file
</Tooltip>
</ActionButton>
<ActionButton
Element="button"
icon={{
icon: 'folderPlus',
iconClassName: '!text-current',
bgClassName: 'bg-transparent',
}}
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
onClick={createFolder}
>
<Tooltip position="bottom-right" delay={750}>
Create folder
</Tooltip>
</ActionButton>
</>
)
}
export const FileTree = ({ className = '', closePanel }: FileTreeProps) => {
return (
<div className={className}>
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/40 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
<ActionButton
Element="button"
icon={{
icon: 'filePlus',
iconClassName: '!text-current',
bgClassName: 'bg-transparent',
}}
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
onClick={createFile}
>
<Tooltip position="bottom-right" delay={750}>
Create file
</Tooltip>
</ActionButton>
<ActionButton
Element="button"
icon={{
icon: 'folderPlus',
iconClassName: '!text-current',
bgClassName: 'bg-transparent',
}}
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
onClick={createFolder}
>
<Tooltip position="bottom-right" delay={750}>
Create folder
</Tooltip>
</ActionButton>
</div>
<div className="overflow-auto max-h-full pb-12">
<ul
className="m-0 p-0 text-sm"
onClickCapture={(e) => {
send({ type: 'Set selected directory', data: context.project })
}}
>
{sortProject(context.project.children || []).map((fileOrDir) => (
<FileTreeItem
project={context.project}
currentFile={file}
fileOrDir={fileOrDir}
closePanel={closePanel}
key={fileOrDir.path}
/>
))}
</ul>
<FileTreeMenu />
</div>
<FileTreeInner onDoubleClick={closePanel} />
</div>
)
}
export const FileTreeInner = ({
onDoubleClick,
}: {
onDoubleClick?: () => void
}) => {
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
const { send, context } = useFileContext()
const documentHasFocus = useDocumentHasFocus()
// Refresh the file tree when the document gets focus
useEffect(() => {
send({ type: 'Refresh' })
}, [documentHasFocus])
return (
<div className="overflow-auto max-h-full pb-12">
<ul
className="m-0 p-0 text-sm"
onClickCapture={(e) => {
send({ type: 'Set selected directory', data: context.project })
}}
>
{sortProject(context.project.children || []).map((fileOrDir) => (
<FileTreeItem
project={context.project}
currentFile={loaderData?.file}
fileOrDir={fileOrDir}
onDoubleClick={onDoubleClick}
key={fileOrDir.path}
/>
))}
</ul>
</div>
)
}

View File

@ -3,7 +3,7 @@ import type * as LSP from 'vscode-languageserver-protocol'
import React, { createContext, useMemo, useEffect, useContext } from 'react'
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
import Client from '../editor/plugins/lsp/client'
import { DEV, TEST } from 'env'
import { TEST, VITE_KC_API_BASE_URL } from 'env'
import kclLanguage from 'editor/plugins/lsp/kcl/language'
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
import { useStore } from 'useStore'
@ -103,7 +103,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
wasmUrl: wasmUrl(),
token: token,
baseUnit: defaultUnit.current,
devMode: DEV,
apiBaseUrl: VITE_KC_API_BASE_URL,
}
lspWorker.postMessage({
worker: LspWorker.Kcl,
@ -177,7 +177,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: CopilotWorkerOptions = {
wasmUrl: wasmUrl(),
token: token,
devMode: DEV,
apiBaseUrl: VITE_KC_API_BASE_URL,
}
lspWorker.postMessage({
worker: LspWorker.Copilot,

View File

@ -56,6 +56,7 @@ import toast from 'react-hot-toast'
import { EditorSelection } from '@uiw/react-codemirror'
import { CoreDumpManager } from 'lib/coredump'
import { useHotkeys } from 'react-hotkeys-hook'
import { useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
type MachineContext<T extends AnyStateMachine> = {
@ -84,7 +85,12 @@ export const ModelingMachineProvider = ({
} = useSettingsAuthContext()
const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null)
let [searchParams] = useSearchParams()
const pool = searchParams.get('pool')
useSetupEngineManager(streamRef, token, {
pool: pool,
theme: theme.current,
highlightEdges: highlightEdges.current,
enableSSAO: enableSSAO.current,

View File

@ -10,21 +10,32 @@ import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEdito
import { CustomIconName } from 'components/CustomIcon'
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
import { ReactNode } from 'react'
import type { PaneType } from 'useStore'
import { MemoryPane } from './MemoryPane'
import { KclErrorsPane, LogsPane } from './LoggingPanes'
import { DebugPane } from './DebugPane'
import { FileTreeInner, FileTreeMenu } from 'components/FileTree'
export type Pane = {
id: PaneType
export type SidebarType =
| 'code'
| 'debug'
| 'export'
| 'files'
| 'kclErrors'
| 'logs'
| 'lspMessages'
| 'variables'
export type SidebarPane = {
id: SidebarType
title: string
icon: CustomIconName | IconDefinition
keybinding: string
Content: ReactNode | React.FC
Menu?: ReactNode | React.FC
keybinding: string
hideOnPlatform?: 'desktop' | 'web'
}
export const topPanes: Pane[] = [
export const topPanes: SidebarPane[] = [
{
id: 'code',
title: 'KCL Code',
@ -33,9 +44,18 @@ export const topPanes: Pane[] = [
keybinding: 'shift + c',
Menu: KclEditorMenu,
},
{
id: 'files',
title: 'Project Files',
icon: 'folder',
Content: FileTreeInner,
keybinding: 'shift + f',
Menu: FileTreeMenu,
hideOnPlatform: 'web',
},
]
export const bottomPanes: Pane[] = [
export const bottomPanes: SidebarPane[] = [
{
id: 'variables',
title: 'Variables',

View File

@ -2,13 +2,19 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Resizable } from 're-resizable'
import { useCallback, useEffect, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { PaneType, useStore } from 'useStore'
import { useStore } from 'useStore'
import { Tab } from '@headlessui/react'
import { Pane, bottomPanes, topPanes } from './ModelingPanes'
import {
SidebarPane,
SidebarType,
bottomPanes,
topPanes,
} from './ModelingPanes'
import Tooltip from 'components/Tooltip'
import { ActionIcon } from 'components/ActionIcon'
import styles from './ModelingSidebar.module.css'
import { ModelingPane } from './ModelingPane'
import { isTauri } from 'lib/isTauri'
interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -52,7 +58,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
}
interface ModelingSidebarSectionProps {
panes: Pane[]
panes: SidebarPane[]
alignButtons?: 'start' | 'end'
}
@ -69,11 +75,11 @@ function ModelingSidebarSection({
}))
const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane))
const [currentPane, setCurrentPane] = useState(
foundOpenPane || ('none' as PaneType | 'none')
foundOpenPane || ('none' as SidebarType | 'none')
)
const togglePane = useCallback(
(newPane: PaneType | 'none') => {
(newPane: SidebarType | 'none') => {
if (newPane === 'none') {
setOpenPanes(openPanes.filter((p) => p !== currentPane))
setCurrentPane('none')
@ -90,9 +96,15 @@ function ModelingSidebarSection({
// Filter out the debug panel if it's not supposed to be shown
// TODO: abstract out for allowing user to configure which panes to show
const filteredPanes = showDebugPanel.current
? panes
: panes.filter((pane) => pane.id !== 'debug')
const filteredPanes = (
showDebugPanel.current ? panes : panes.filter((pane) => pane.id !== 'debug')
).filter(
(pane) =>
!pane.hideOnPlatform ||
(isTauri()
? pane.hideOnPlatform === 'web'
: pane.hideOnPlatform === 'desktop')
)
useEffect(() => {
if (
!showDebugPanel.current &&
@ -168,8 +180,8 @@ function ModelingSidebarSection({
}
interface ModelingPaneButtonProps {
paneConfig: Pane
currentPane: PaneType | 'none'
paneConfig: SidebarPane
currentPane: SidebarType | 'none'
togglePane: () => void
}

View File

@ -8,13 +8,13 @@ export interface KclWorkerOptions {
wasmUrl: string
token: string
baseUnit: UnitLength
devMode: boolean
apiBaseUrl: string
}
export interface CopilotWorkerOptions {
wasmUrl: string
token: string
devMode: boolean
apiBaseUrl: string
}
export enum LspWorkerEventType {

View File

@ -28,11 +28,11 @@ const initialise = async (wasmUrl: string) => {
export async function copilotLspRun(
config: ServerConfig,
token: string,
devMode: boolean = false
baseUrl: string
) {
try {
console.log('starting copilot lsp')
await copilot_lsp_run(config, token, devMode)
await copilot_lsp_run(config, token, baseUrl)
} catch (e: any) {
console.log('copilot lsp failed', e)
// We can't restart here because a moved value, we should do this another way.
@ -44,11 +44,11 @@ export async function kclLspRun(
engineCommandManager: EngineCommandManager | null,
token: string,
baseUnit: string,
devMode: boolean = false
baseUrl: string
) {
try {
console.log('start kcl lsp')
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, devMode)
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, baseUrl)
} catch (e: any) {
console.log('kcl lsp failed', e)
// We can't restart here because a moved value, we should do this another way.
@ -80,12 +80,12 @@ onmessage = function (event) {
null,
kclData.token,
kclData.baseUnit,
kclData.devMode
kclData.apiBaseUrl
)
break
case LspWorker.Copilot:
let copilotData = eventData as CopilotWorkerOptions
copilotLspRun(config, copilotData.token, copilotData.devMode)
copilotLspRun(config, copilotData.token, copilotData.apiBaseUrl)
break
}
})

View File

@ -9,10 +9,12 @@ export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>,
token?: string,
settings = {
pool: null,
theme: Themes.System,
highlightEdges: true,
enableSSAO: true,
} as {
pool: string | null
theme: Themes
highlightEdges: boolean
enableSSAO: boolean
@ -35,6 +37,12 @@ export function useSetupEngineManager(
const hasSetNonZeroDimensions = useRef<boolean>(false)
if (settings.pool) {
// override the pool param (?pool=) to request a specific engine instance
// from a particular pool.
engineCommandManager.pool = settings.pool
}
useLayoutEffect(() => {
// Load the engine command manager once with the initial width and height,
// then we do not want to reload it.

View File

@ -888,6 +888,7 @@ export class EngineCommandManager {
sceneCommandArtifacts: ArtifactMap = {}
outSequence = 1
inSequence = 1
pool?: string
engineConnection?: EngineConnection
defaultPlanes: DefaultPlanes | null = null
commandLogs: CommandLog[] = []
@ -914,8 +915,9 @@ export class EngineCommandManager {
callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] =
[]
constructor() {
constructor(pool?: string) {
this.engineConnection = undefined
this.pool = pool
}
private _camControlsCameraChange = () => {}
@ -972,7 +974,8 @@ export class EngineCommandManager {
}
const additionalSettings = settings.enableSSAO ? '&post_effect=ssao' : ''
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}`
const pool = this.pool == undefined ? '' : `&pool=${this.pool}`
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}${pool}`
this.engineConnection = new EngineConnection({
engineCommandManager: this,
url,

View File

@ -14,6 +14,7 @@ import init, {
parse_app_settings,
parse_project_settings,
default_project_settings,
parse_project_route,
} from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
@ -31,6 +32,7 @@ import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { TEST } from 'env'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Value } from '../wasm-lib/kcl/bindings/Value'
@ -389,3 +391,18 @@ export function parseProjectSettings(toml: string): ProjectConfiguration {
throw new Error(`Error parsing project settings: ${e}`)
}
}
export function parseProjectRoute(
configuration: Configuration,
route_str: string
): ProjectRoute {
try {
const route: ProjectRoute = parse_project_route(
JSON.stringify(configuration),
route_str
)
return route
} catch (e: any) {
throw new Error(`Error parsing project route: ${e}`)
}
}

View File

@ -49,6 +49,11 @@ export class CoreDumpManager {
return APP_VERSION
}
// Get the backend pool we've requested.
pool(): string {
return this.engineCommandManager.pool || ''
}
// Get the os information.
getOsInfo(): Promise<string> {
if (this.isTauri()) {

View File

@ -1,7 +1,11 @@
import { sep } from '@tauri-apps/api/path'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { BROWSER_FILE_NAME, BROWSER_PROJECT_NAME, FILE_EXT } from './constants'
import { isTauri } from './isTauri'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
import { parseProjectRoute, readAppSettingsFile } from './tauri'
import { parseProjectRoute as parseProjectRouteWasm } from 'lang/wasm'
import { readLocalStorageAppSettingsFile } from './settings/settingsUtils'
const prependRoutes =
(routesObject: Record<string, string>) => (prepend: string) => {
@ -25,28 +29,23 @@ export const paths = {
} as const
export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}`
export function getProjectMetaByRouteId(id?: string, defaultDir = '') {
export async function getProjectMetaByRouteId(
id?: string,
configuration?: Configuration
): Promise<ProjectRoute | undefined> {
if (!id) return undefined
const s = isTauri() ? sep() : '/'
const decodedId = decodeURIComponent(id).replace(/\/$/, '') // remove trailing slash
const projectAndFile =
defaultDir === '/'
? decodedId.replace(defaultDir, '')
: decodedId.replace(defaultDir + s, '')
const filePathParts = projectAndFile.split(s)
const projectName = filePathParts[0]
const projectPath =
(defaultDir === '/' ? defaultDir : defaultDir + s) + projectName
const lastPathPart = filePathParts[filePathParts.length - 1]
const currentFileName =
lastPathPart === projectName ? undefined : lastPathPart
const currentFilePath = lastPathPart === projectName ? undefined : decodedId
const inTauri = isTauri()
return {
projectName,
projectPath,
currentFileName,
currentFilePath,
if (!configuration) {
configuration = inTauri
? await readAppSettingsFile()
: readLocalStorageAppSettingsFile()
}
const route = inTauri
? await parseProjectRoute(configuration, id)
: parseProjectRouteWasm(configuration, id)
return route
}

View File

@ -25,17 +25,21 @@ import { createSettings } from './settings/initialSettings'
// occurred during the settings load
export const settingsLoader: LoaderFunction = async ({
params,
}): Promise<ReturnType<typeof createSettings>> => {
let { settings } = await loadAndValidateSettings()
}): Promise<
ReturnType<typeof createSettings> | ReturnType<typeof redirect>
> => {
let { settings, configuration } = await loadAndValidateSettings()
// I don't love that we have to read the settings again here,
// but we need to get the project path to load the project settings
if (params.id) {
const defaultDir = settings.app.projectDirectory.current || ''
const projectPathData = getProjectMetaByRouteId(params.id, defaultDir)
const projectPathData = await getProjectMetaByRouteId(
params.id,
configuration
)
if (projectPathData) {
const { projectName } = projectPathData
const { settings: s } = await loadAndValidateSettings(projectName)
const { project_name } = projectPathData
const { settings: s } = await loadAndValidateSettings(project_name)
settings = s
}
}
@ -69,17 +73,19 @@ export const onboardingRedirectLoader: ActionFunction = async (args) => {
export const fileLoader: LoaderFunction = async ({
params,
}): Promise<FileLoaderData | Response> => {
let { settings } = await loadAndValidateSettings()
let { configuration } = await loadAndValidateSettings()
const defaultDir = settings.app.projectDirectory.current || '/'
const projectPathData = getProjectMetaByRouteId(params.id, defaultDir)
const projectPathData = await getProjectMetaByRouteId(
params.id,
configuration
)
const isBrowserProject = params.id === decodeURIComponent(BROWSER_PATH)
if (!isBrowserProject && projectPathData) {
const { projectName, projectPath, currentFileName, currentFilePath } =
const { project_name, project_path, current_file_name, current_file_path } =
projectPathData
if (!currentFileName || !currentFilePath) {
if (!current_file_name || !current_file_path || !project_name) {
return redirect(
`${paths.FILE}/${encodeURIComponent(
`${params.id}${isTauri() ? sep() : '/'}${PROJECT_ENTRYPOINT}`
@ -89,33 +95,33 @@ export const fileLoader: LoaderFunction = async ({
// TODO: PROJECT_ENTRYPOINT is hardcoded
// until we support setting a project's entrypoint file
const code = await readTextFile(currentFilePath)
const code = await readTextFile(current_file_path)
// Update both the state and the editor's code.
// We explicitly do not write to the file here since we are loading from
// the file system and not the editor.
codeManager.updateCurrentFilePath(currentFilePath)
codeManager.updateCurrentFilePath(current_file_path)
codeManager.updateCodeStateEditor(code)
kclManager.executeCode(true)
// Set the file system manager to the project path
// So that WASM gets an updated path for operations
fileSystemManager.dir = projectPath
fileSystemManager.dir = project_path
const projectData: IndexLoaderData = {
code,
project: isTauri()
? await getProjectInfo(projectName)
? await getProjectInfo(project_path, configuration)
: {
name: projectName,
path: projectPath,
name: project_name,
path: project_path,
children: [],
kcl_file_count: 0,
directory_count: 0,
},
file: {
name: currentFileName,
path: currentFilePath,
name: current_file_name,
path: current_file_path,
children: [],
},
}

View File

@ -101,7 +101,7 @@ function localStorageProjectSettingsPath() {
return '/' + BROWSER_PROJECT_NAME + '/project.toml'
}
function readLocalStorageAppSettingsFile(): Configuration {
export function readLocalStorageAppSettingsFile(): Configuration {
// TODO: Remove backwards compatibility after a few releases.
let stored =
localStorage.getItem(localStorageAppSettingsPath()) ??

View File

@ -6,6 +6,18 @@ import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
// Get the app state from tauri.
export async function getState(): Promise<ProjectState | undefined> {
return await invoke<ProjectState | undefined>('get_state')
}
// Set the app state in tauri.
export async function setState(state: ProjectState | undefined): Promise<void> {
return await invoke('set_state', { state })
}
// Get the initial default dir for holding all projects.
export async function getInitialDefaultDir(): Promise<string> {
@ -53,7 +65,7 @@ export async function listProjects(
}
export async function getProjectInfo(
projectName: string,
projectPath: string,
configuration?: Configuration
): Promise<Project> {
if (!configuration) {
@ -61,7 +73,7 @@ export async function getProjectInfo(
}
return await invoke<Project>('get_project_info', {
configuration,
projectName,
projectPath,
})
}
@ -69,6 +81,16 @@ export async function login(host: string): Promise<string> {
return await invoke('login', { host })
}
export async function parseProjectRoute(
configuration: Configuration,
route: string
): Promise<ProjectRoute> {
return await invoke<ProjectRoute>('parse_project_route', {
configuration,
route,
})
}
export async function getUser(
token: string | undefined,
host: string

View File

@ -9,6 +9,7 @@ import {
import { enginelessExecutor } from './lib/testHelpers'
import { EngineCommandManager } from './lang/std/engineConnection'
import { KCLError } from './lang/errors'
import { SidebarType } from 'components/ModelingSidebar/ModelingPanes'
export type ToolTip =
| 'lineTo'
@ -44,14 +45,6 @@ export const toolTips = [
'tangentialArcTo',
] as any as ToolTip[]
export type PaneType =
| 'code'
| 'variables'
| 'debug'
| 'kclErrors'
| 'logs'
| 'lspMessages'
export interface StoreState {
mediaStream?: MediaStream
setMediaStream: (mediaStream: MediaStream) => void
@ -77,8 +70,8 @@ export interface StoreState {
showHomeMenu: boolean
setHomeShowMenu: (showMenu: boolean) => void
openPanes: PaneType[]
setOpenPanes: (panes: PaneType[]) => void
openPanes: SidebarType[]
setOpenPanes: (panes: SidebarType[]) => void
homeMenuItems: {
name: string
path: string

View File

@ -1895,7 +1895,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.1.51"
version = "0.1.53"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1974,6 +1974,7 @@ dependencies = [
"bigdecimal",
"bytes",
"chrono",
"clap",
"data-encoding",
"format_serde_error",
"futures",
@ -4726,6 +4727,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bson",
"clap",
"console_error_panic_hook",
"futures",
"gloo-utils",

View File

@ -11,6 +11,7 @@ crate-type = ["cdylib"]
[dependencies]
bson = { version = "2.10.0", features = ["uuid-1", "chrono"] }
clap = "4.5.4"
gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" }
kittycad = { workspace = true }

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.1.51"
version = "0.1.53"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -16,7 +16,7 @@ async-recursion = "1.1.0"
async-trait = "0.1.80"
base64 = "0.22.0"
chrono = "0.4.38"
clap = { version = "4.5.4", features = ["cargo", "derive", "env", "unicode"], optional = true }
clap = { version = "4.5.4", default-features = false, optional = true }
dashmap = "5.5.3"
databake = { version = "0.1.7", features = ["derive"] }
derive-docs = { version = "0.1.17", path = "../derive-docs" }
@ -24,7 +24,7 @@ form_urlencoded = "1.2.1"
futures = { version = "0.3.30" }
git_rev = "0.1.0"
gltf-json = "1.4.0"
kittycad = { workspace = true }
kittycad = { workspace = true, features = ["clap"] }
kittycad-execution-plan-macros = { workspace = true }
kittycad-execution-plan-traits = { workspace = true }
lazy_static = "1.4.0"
@ -61,7 +61,7 @@ tokio-tungstenite = { version = "0.21.0", features = ["rustls-tls-native-roots"]
tower-lsp = { version = "0.20.0", features = ["proposed"] }
[features]
default = ["engine"]
default = ["cli", "engine"]
cli = ["dep:clap"]
engine = []

View File

@ -33,6 +33,10 @@ impl CoreDump for CoreDumper {
Ok(env!("CARGO_PKG_VERSION").to_string())
}
fn pool(&self) -> Result<String> {
Ok("".to_owned())
}
async fn os(&self) -> Result<crate::coredump::OsInfo> {
Ok(crate::coredump::OsInfo {
platform: Some(std::env::consts::OS.to_string()),

View File

@ -19,6 +19,8 @@ pub trait CoreDump: Clone {
fn version(&self) -> Result<String>;
fn pool(&self) -> Result<String>;
async fn os(&self) -> Result<OsInfo>;
fn is_tauri(&self) -> Result<bool>;
@ -71,6 +73,7 @@ pub trait CoreDump: Clone {
os,
webrtc_stats,
github_issue_url: None,
pool: self.pool()?,
};
app_info.set_github_issue_url(&screenshot_url)?;
@ -103,6 +106,9 @@ pub struct AppInfo {
/// This gets prepoulated with all the core dump info.
#[serde(skip_serializing_if = "Option::is_none")]
pub github_issue_url: Option<String>,
/// Engine pool the client is connected to.
pub pool: String,
}
impl AppInfo {

View File

@ -16,6 +16,9 @@ extern "C" {
#[wasm_bindgen(method, js_name = baseApiUrl, catch)]
fn baseApiUrl(this: &CoreDumpManager) -> Result<String, js_sys::Error>;
#[wasm_bindgen(method, js_name = pool, catch)]
fn pool(this: &CoreDumpManager) -> Result<String, js_sys::Error>;
#[wasm_bindgen(method, js_name = version, catch)]
fn version(this: &CoreDumpManager) -> Result<String, js_sys::Error>;
@ -66,6 +69,12 @@ impl CoreDump for CoreDumper {
.map_err(|e| anyhow::anyhow!("Failed to get response from version: {:?}", e))
}
fn pool(&self) -> Result<String> {
self.manager
.pool()
.map_err(|e| anyhow::anyhow!("Failed to get response from pool: {:?}", e))
}
async fn os(&self) -> Result<crate::coredump::OsInfo> {
let promise = self
.manager

View File

@ -5,6 +5,111 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::Configuration;
/// State management for the application.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct ProjectState {
pub project: Project,
pub current_file: Option<String>,
}
/// Project route information.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct ProjectRoute {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_name: Option<String>,
pub project_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_file_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_file_path: Option<String>,
}
impl ProjectRoute {
/// Get the project state from the url in the route.
pub fn from_route(configuration: &Configuration, route: &str) -> Result<Self> {
let path = std::path::Path::new(route);
// Check if the default project path is in the route.
let (project_path, project_name) = if path.starts_with(&configuration.settings.project.directory)
&& configuration.settings.project.directory != std::path::PathBuf::default()
{
// Get the project name.
if let Some(project_name) = path
.strip_prefix(&configuration.settings.project.directory)
.unwrap()
.iter()
.next()
{
(
configuration
.settings
.project
.directory
.join(project_name)
.display()
.to_string(),
Some(project_name.to_string_lossy().to_string()),
)
} else {
(configuration.settings.project.directory.display().to_string(), None)
}
} else {
// Assume the project path is the parent directory of the file.
let project_dir = if path.display().to_string().ends_with(".kcl") {
path.parent()
.ok_or_else(|| anyhow::anyhow!("Parent directory not found: {}", path.display()))?
} else {
path
};
if project_dir == std::path::Path::new("/") {
(
path.display().to_string(),
Some(
path.file_name()
.ok_or_else(|| anyhow::anyhow!("File name not found: {}", path.display()))?
.to_string_lossy()
.to_string(),
),
)
} else if let Some(project_name) = project_dir.file_name() {
(
project_dir.display().to_string(),
Some(project_name.to_string_lossy().to_string()),
)
} else {
(project_dir.display().to_string(), None)
}
};
let (current_file_name, current_file_path) = if path.display().to_string() == project_path {
(None, None)
} else {
(
Some(
path.file_name()
.ok_or_else(|| anyhow::anyhow!("File name not found: {}", path.display()))?
.to_string_lossy()
.to_string(),
),
Some(path.display().to_string()),
)
};
Ok(Self {
project_name,
project_path,
current_file_name,
current_file_path,
})
}
}
/// Information about project.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
@ -23,6 +128,34 @@ pub struct Project {
}
impl Project {
#[cfg(not(target_arch = "wasm32"))]
/// Populate a project from a path.
pub async fn from_path<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
// Check if they are using '.' as the path.
let path = if path.as_ref() == std::path::Path::new(".") {
std::env::current_dir()?
} else {
path.as_ref().to_path_buf()
};
// Make sure the path exists.
if !path.exists() {
return Err(anyhow::anyhow!("Path does not exist"));
}
let file = crate::settings::utils::walk_dir(&path).await?;
let metadata = std::fs::metadata(path).ok().map(|m| m.into());
let mut project = Self {
file,
metadata,
kcl_file_count: 0,
directory_count: 0,
};
project.populate_kcl_file_count()?;
project.populate_directory_count()?;
Ok(project)
}
/// Populate the number of KCL files in the project.
pub fn populate_kcl_file_count(&mut self) -> Result<()> {
let mut count = 0;
@ -196,3 +329,141 @@ impl From<std::fs::Metadata> for FileMetadata {
}
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
#[test]
fn test_project_route_from_route_std_path() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/main.kcl";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("assembly".to_string()),
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
current_file_name: Some("main.kcl".to_string()),
current_file_path: Some(
"/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/main.kcl".to_string()
),
}
);
}
#[test]
fn test_project_route_from_route_std_path_dir() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("assembly".to_string()),
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
current_file_name: None,
current_file_path: None,
}
);
}
#[test]
fn test_project_route_from_route_std_path_dir_empty() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: None,
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects".to_string(),
current_file_name: None,
current_file_path: None,
}
);
}
#[test]
fn test_project_route_from_route_outside_std_path() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/kittycad/modeling-app/main.kcl";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("modeling-app".to_string()),
project_path: "/Users/macinatormax/kittycad/modeling-app".to_string(),
current_file_name: Some("main.kcl".to_string()),
current_file_path: Some("/Users/macinatormax/kittycad/modeling-app/main.kcl".to_string()),
}
);
}
#[test]
fn test_project_route_from_route_outside_std_path_dir() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/kittycad/modeling-app";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("modeling-app".to_string()),
project_path: "/Users/macinatormax/kittycad/modeling-app".to_string(),
current_file_name: None,
current_file_path: None,
}
);
}
#[test]
fn test_project_route_from_route_browser() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory = std::path::PathBuf::default();
let route = "/browser/main.kcl";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("browser".to_string()),
project_path: "/browser".to_string(),
current_file_name: Some("main.kcl".to_string()),
current_file_path: Some("/browser/main.kcl".to_string()),
}
);
}
#[test]
fn test_project_route_from_route_browser_no_path() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory = std::path::PathBuf::default();
let route = "/browser";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("browser".to_string()),
project_path: "/browser".to_string(),
current_file_name: None,
current_file_path: None,
}
);
}
}

View File

@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
use validator::{Validate, ValidateRange};
const DEFAULT_THEME_COLOR: f64 = 264.5;
const DEFAULT_PROJECT_KCL_FILE: &str = "main.kcl";
pub const DEFAULT_PROJECT_KCL_FILE: &str = "main.kcl";
const DEFAULT_PROJECT_NAME_TEMPLATE: &str = "project-$nnn";
/// High level configuration.
@ -129,7 +129,7 @@ impl Configuration {
continue;
}
projects.push(self.get_project_info(&e.file_name().to_string_lossy()).await?);
projects.push(self.get_project_info(&e.path().display().to_string()).await?);
}
Ok(projects)
@ -137,21 +137,20 @@ impl Configuration {
#[cfg(not(target_arch = "wasm32"))]
/// Get information about a project.
pub async fn get_project_info(&self, project_name: &str) -> Result<crate::settings::types::file::Project> {
let main_dir = &self.ensure_project_directory_exists().await?;
if project_name.is_empty() {
return Err(anyhow::anyhow!("Project name cannot be empty."));
pub async fn get_project_info(&self, project_path: &str) -> Result<crate::settings::types::file::Project> {
// Check the directory.
let project_dir = std::path::Path::new(project_path);
if !project_dir.exists() {
return Err(anyhow::anyhow!("Project directory does not exist: {}", project_path));
}
// Check the directory.
let project_dir = main_dir.join(project_name);
if !project_dir.exists() {
return Err(anyhow::anyhow!("Project directory does not exist."));
// Make sure it is a directory.
if !project_dir.is_dir() {
return Err(anyhow::anyhow!("Project path is not a directory: {}", project_path));
}
let mut project = crate::settings::types::file::Project {
file: crate::settings::utils::walk_dir(&project_dir).await?,
file: crate::settings::utils::walk_dir(project_dir).await?,
metadata: Some(tokio::fs::metadata(&project_dir).await?.into()),
kcl_file_count: 0,
directory_count: 0,

View File

@ -1,31 +1,54 @@
//! Utility functions for settings.
use std::path::PathBuf;
use std::path::Path;
use anyhow::Result;
use clap::ValueEnum;
use crate::settings::types::file::FileEntry;
lazy_static::lazy_static! {
static ref RELEVANT_EXTENSIONS: Vec<String> = {
let mut relevant_extensions = vec!["kcl".to_string(), "stp".to_string(), "glb".to_string()];
let named_extensions = kittycad::types::FileImportFormat::value_variants()
.iter()
.map(|x| format!("{}", x))
.collect::<Vec<String>>();
// Add all the default import formats.
relevant_extensions.extend_from_slice(&named_extensions);
relevant_extensions
};
}
/// Walk a directory recursively and return a list of all files.
#[async_recursion::async_recursion]
pub async fn walk_dir(dir: &PathBuf) -> Result<FileEntry> {
pub async fn walk_dir<P: AsRef<Path> + Send>(dir: P) -> Result<FileEntry> {
let mut entry = FileEntry {
name: dir
.as_ref()
.file_name()
.ok_or_else(|| anyhow::anyhow!("No file name"))?
.to_string_lossy()
.to_string(),
path: dir.display().to_string(),
path: dir.as_ref().display().to_string(),
children: None,
};
let mut children = vec![];
let mut entries = tokio::fs::read_dir(&dir).await?;
let mut entries = tokio::fs::read_dir(&dir.as_ref()).await?;
while let Some(e) = entries.next_entry().await? {
// ignore hidden files and directories (starting with a dot)
if e.file_name().to_string_lossy().starts_with('.') {
continue;
}
if e.file_type().await?.is_dir() {
children.push(walk_dir(&e.path()).await?);
} else {
if !is_relevant_file(&e.path())? {
continue;
}
children.push(FileEntry {
name: e.file_name().to_string_lossy().to_string(),
path: e.path().display().to_string(),
@ -34,9 +57,17 @@ pub async fn walk_dir(dir: &PathBuf) -> Result<FileEntry> {
}
}
if !children.is_empty() {
entry.children = Some(children);
}
// We don't set this to none if there are no children, because it's a directory.
entry.children = Some(children);
Ok(entry)
}
/// Check if a file is relevant for the application.
fn is_relevant_file<P: AsRef<Path>>(path: P) -> Result<bool> {
if let Some(ext) = path.as_ref().extension() {
Ok(RELEVANT_EXTENSIONS.contains(&ext.to_string_lossy().to_string()))
} else {
Ok(false)
}
}

View File

@ -198,7 +198,7 @@ pub async fn kcl_lsp_run(
engine_manager: Option<kcl_lib::engine::conn_wasm::EngineCommandManager>,
units: &str,
token: String,
is_dev: bool,
baseurl: String,
) -> Result<(), JsValue> {
console_error_panic_hook::set_once();
@ -216,9 +216,7 @@ pub async fn kcl_lsp_run(
let token_types = kcl_lib::token::TokenType::all_semantic_token_types().unwrap();
let mut zoo_client = kittycad::Client::new(token);
if is_dev {
zoo_client.set_base_url("https://api.dev.zoo.dev");
}
zoo_client.set_base_url(baseurl.as_str());
let file_manager = Arc::new(kcl_lib::fs::FileManager::new(fs));
@ -313,7 +311,7 @@ pub async fn kcl_lsp_run(
// NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
#[wasm_bindgen]
pub async fn copilot_lsp_run(config: ServerConfig, token: String, is_dev: bool) -> Result<(), JsValue> {
pub async fn copilot_lsp_run(config: ServerConfig, token: String, baseurl: String) -> Result<(), JsValue> {
console_error_panic_hook::set_once();
let ServerConfig {
@ -323,9 +321,7 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String, is_dev: bool)
} = config;
let mut zoo_client = kittycad::Client::new(token);
if is_dev {
zoo_client.set_base_url("https://api.dev.zoo.dev");
}
zoo_client.set_base_url(baseurl.as_str());
let file_manager = Arc::new(kcl_lib::fs::FileManager::new(fs));
@ -511,3 +507,19 @@ pub fn parse_project_settings(toml_str: &str) -> Result<JsValue, String> {
// gloo-serialize crate instead.
JsValue::from_serde(&settings).map_err(|e| e.to_string())
}
/// Parse the project route.
#[wasm_bindgen]
pub fn parse_project_route(configuration: &str, route: &str) -> Result<JsValue, String> {
console_error_panic_hook::set_once();
let configuration: kcl_lib::settings::types::Configuration =
serde_json::from_str(configuration).map_err(|e| e.to_string())?;
let route =
kcl_lib::settings::types::file::ProjectRoute::from_route(&configuration, route).map_err(|e| e.to_string())?;
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&route).map_err(|e| e.to_string())
}