DX components command (#4669)
* add component subcommand * add dx component add, remove and list commands * handle rust dependencies * handle component dependencies * fetch remote registries * sort components before listing them * remove local dependency * move te component manifest into the dioxus workspace * add additional instalation instructions the first time the component module is created * fix mod name * fix clippy * improve docs and move to the main branch of components * fix typos * more typos * undo const-serialize changes * remove a few unwraps * Allow adding multiple components at once and add a `--all` flag * add options to the config for the base path, global asset path and default registry * component_path -> component_dir * component_dir is always relative to the crate root * fix clippy * fix request imports * wip: code style changes * rename to "components" instead of "component" * bump lock --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
1 parent
b3966c3509
commit
79613f9db9
18 files changed
+1101
-17
No files matched your search
Generated
+39
-3
@@ -5333,6 +5333,7 @@ dependencies = [
|
||||
"dioxus-cli-config",
|
||||
"dioxus-cli-opt",
|
||||
"dioxus-cli-telemetry",
|
||||
"dioxus-component-manifest",
|
||||
"dioxus-core",
|
||||
"dioxus-core-types",
|
||||
"dioxus-devtools-types",
|
||||
@@ -5351,6 +5352,7 @@ dependencies = [
|
||||
"fs2",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"git2",
|
||||
"handlebars",
|
||||
"headers",
|
||||
"html_parser",
|
||||
@@ -5496,6 +5498,15 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-component-manifest"
|
||||
version = "0.7.0-rc.2"
|
||||
dependencies = [
|
||||
"schemars 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-config-macro"
|
||||
version = "0.7.0-rc.2"
|
||||
@@ -14694,10 +14705,23 @@ checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
@@ -15052,15 +15076,27 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
name = "serde_derive_internals"
|
||||
version = "0.29.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -69,6 +69,7 @@ members = [
|
||||
"packages/native-dom",
|
||||
"packages/asset-resolver",
|
||||
"packages/depinfo",
|
||||
"packages/component-manifest",
|
||||
|
||||
# CLI harnesses, all included
|
||||
"packages/cli-harnesses/*",
|
||||
@@ -182,6 +183,7 @@ dioxus-native = { path = "packages/native", version = "0.7.0-rc.2" }
|
||||
dioxus-native-dom = { path = "packages/native-dom", version = "0.7.0-rc.2" }
|
||||
dioxus-asset-resolver = { path = "packages/asset-resolver", version = "0.7.0-rc.2" }
|
||||
dioxus-config-macros = { path = "packages/config-macros", version = "0.7.0-rc.2" }
|
||||
dioxus-component-manifest = { path = "packages/component-manifest", version = "0.7.0-rc.2" }
|
||||
|
||||
generational-box = { path = "packages/generational-box", version = "0.7.0-rc.2" }
|
||||
lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.7.0-rc.2" }
|
||||
|
||||
@@ -27,6 +27,7 @@ wasm-split-cli = { workspace = true }
|
||||
depinfo = { workspace = true }
|
||||
subsecond-types = { workspace = true }
|
||||
dioxus-cli-telemetry = { workspace = true }
|
||||
dioxus-component-manifest = { workspace = true }
|
||||
|
||||
clap = { workspace = true, features = ["derive", "cargo"] }
|
||||
convert_case = { workspace = true }
|
||||
@@ -78,6 +79,7 @@ dunce = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["rustls-tls", "trust-dns", "json"] }
|
||||
tower = { workspace = true }
|
||||
git2 = "0.20.2"
|
||||
|
||||
# path lookup
|
||||
which = { version = "8.0.0" }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
serve::WebServer, BuildArtifacts, BuildRequest, BuildStage, BuilderUpdate, BundleFormat,
|
||||
ProgressRx, ProgressTx, Result, RustcArgs, StructuredOutput,
|
||||
serve::WebServer, verbosity_or_default, BuildArtifacts, BuildRequest, BuildStage,
|
||||
BuilderUpdate, BundleFormat, ProgressRx, ProgressTx, Result, RustcArgs, StructuredOutput,
|
||||
};
|
||||
use anyhow::{bail, Context};
|
||||
use dioxus_cli_opt::process_file_to;
|
||||
@@ -489,11 +489,7 @@ impl AppBuilder {
|
||||
));
|
||||
}
|
||||
|
||||
if crate::VERBOSITY
|
||||
.get()
|
||||
.map(|f| f.verbose)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if verbosity_or_default().verbose {
|
||||
envs.push(("RUST_BACKTRACE".into(), "1".to_string()));
|
||||
}
|
||||
|
||||
|
||||
@@ -2567,7 +2567,7 @@ impl BuildRequest {
|
||||
cargo_args.push(self.executable_name().to_string());
|
||||
|
||||
// Set offline/locked/frozen
|
||||
let lock_opts = crate::VERBOSITY.get().cloned().unwrap_or_default();
|
||||
let lock_opts = crate::verbosity_or_default();
|
||||
if lock_opts.frozen {
|
||||
cargo_args.push("--frozen".to_string());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,821 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ops::Deref,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::{verbosity_or_default, DioxusConfig, Result, StructuredOutput, Workspace};
|
||||
use anyhow::{bail, Context};
|
||||
use clap::Parser;
|
||||
use dioxus_component_manifest::{
|
||||
component_manifest_schema, CargoDependency, Component, ComponentDependency,
|
||||
};
|
||||
use git2::Repository;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::{process::Command, task::JoinSet};
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
pub enum ComponentCommand {
|
||||
/// Add a component from a registry
|
||||
Add {
|
||||
#[clap(flatten)]
|
||||
component: ComponentArgs,
|
||||
|
||||
/// The registry to use
|
||||
#[clap(flatten)]
|
||||
registry: Option<ComponentRegistry>,
|
||||
|
||||
/// Overwrite the component if it already exists
|
||||
#[clap(long)]
|
||||
force: bool,
|
||||
},
|
||||
|
||||
/// Remove a component
|
||||
Remove {
|
||||
#[clap(flatten)]
|
||||
component: ComponentArgs,
|
||||
|
||||
/// The registry to use
|
||||
#[clap(flatten)]
|
||||
registry: Option<ComponentRegistry>,
|
||||
},
|
||||
|
||||
/// Update a component registry
|
||||
Update {
|
||||
/// The registry to update
|
||||
#[clap(flatten)]
|
||||
registry: Option<RemoteComponentRegistry>,
|
||||
},
|
||||
|
||||
/// List available components in a registry
|
||||
List {
|
||||
/// The registry to list components in
|
||||
#[clap(flatten)]
|
||||
registry: Option<ComponentRegistry>,
|
||||
},
|
||||
|
||||
/// Clear the component registry cache
|
||||
Clean,
|
||||
|
||||
/// Print the schema for component manifests
|
||||
Schema,
|
||||
}
|
||||
|
||||
/// Arguments for a component and component module location
|
||||
#[derive(Clone, Debug, Parser, Serialize)]
|
||||
pub struct ComponentArgs {
|
||||
/// The components to add or remove
|
||||
#[clap(required_unless_present = "all", value_delimiter = ',')]
|
||||
components: Vec<String>,
|
||||
|
||||
/// The location of the component module in your project (default: src/components)
|
||||
#[clap(long)]
|
||||
module_path: Option<PathBuf>,
|
||||
|
||||
/// The location of the global assets in your project (default: assets)
|
||||
#[clap(long)]
|
||||
global_assets_path: Option<PathBuf>,
|
||||
|
||||
/// Include all components in the registry
|
||||
#[clap(long)]
|
||||
all: bool,
|
||||
}
|
||||
|
||||
impl ComponentCommand {
|
||||
/// Run the component command
|
||||
pub async fn run(self) -> Result<StructuredOutput> {
|
||||
match self {
|
||||
// List all components in the registry
|
||||
Self::List { registry } => {
|
||||
let config = Self::resolve_config().await?;
|
||||
let registry = Self::resolve_registry(registry, &config)?;
|
||||
let mut components = registry.read_components().await?;
|
||||
components.sort_by_key(|c| c.name.clone());
|
||||
for component in components {
|
||||
println!("- {}: {}", component.name, component.description);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a component to the managed component module
|
||||
Self::Add {
|
||||
component: component_args,
|
||||
registry,
|
||||
force,
|
||||
} => {
|
||||
// Resolve the config
|
||||
let config = Self::resolve_config().await?;
|
||||
|
||||
// Resolve the registry
|
||||
let registry = Self::resolve_registry(registry, &config)?;
|
||||
|
||||
// Read all components from the registry
|
||||
let components = registry.read_components().await?;
|
||||
let mode = if force {
|
||||
ComponentExistsBehavior::Overwrite
|
||||
} else {
|
||||
ComponentExistsBehavior::Error
|
||||
};
|
||||
|
||||
// Find the requested components
|
||||
let components = if component_args.all {
|
||||
components
|
||||
} else {
|
||||
component_args
|
||||
.components
|
||||
.iter()
|
||||
.map(|component| find_component(&components, component))
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
};
|
||||
|
||||
// Find and initialize the components module if it doesn't exist
|
||||
let components_root =
|
||||
components_root(component_args.module_path.as_deref(), &config)?;
|
||||
let new_components_module =
|
||||
ensure_components_module_exists(&components_root).await?;
|
||||
|
||||
// Recursively add dependencies
|
||||
// A map of the components that have been added or are queued to be added
|
||||
let mut required_components = HashMap::new();
|
||||
required_components.extend(components.iter().cloned().map(|c| (c, mode)));
|
||||
// A stack of components to process
|
||||
let mut queued_components = components;
|
||||
while let Some(queued_component) = queued_components.pop() {
|
||||
for dependency in &queued_component.component_dependencies {
|
||||
let (registry, name) = match dependency {
|
||||
ComponentDependency::Builtin(name) => {
|
||||
(ComponentRegistry::default(), name)
|
||||
}
|
||||
ComponentDependency::ThirdParty { name, git, rev } => (
|
||||
ComponentRegistry {
|
||||
remote: RemoteComponentRegistry {
|
||||
git: Some(git.clone()),
|
||||
rev: rev.clone(),
|
||||
},
|
||||
path: None,
|
||||
},
|
||||
name,
|
||||
),
|
||||
};
|
||||
let registry_components = registry.read_components().await?;
|
||||
let dependency_component = find_component(®istry_components, name)?;
|
||||
if required_components
|
||||
.insert(
|
||||
dependency_component.clone(),
|
||||
ComponentExistsBehavior::Return,
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
queued_components.push(dependency_component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then collect all required rust dependencies
|
||||
let mut rust_dependencies = HashSet::new();
|
||||
for component in required_components.keys() {
|
||||
rust_dependencies.extend(component.cargo_dependencies.iter().cloned());
|
||||
}
|
||||
|
||||
// And add them to Cargo.toml
|
||||
Self::add_rust_dependencies(&rust_dependencies).await?;
|
||||
|
||||
// Once we have all required components, add them
|
||||
for (component, mode) in required_components {
|
||||
add_component(
|
||||
component_args.global_assets_path.as_deref(),
|
||||
component_args.module_path.as_deref(),
|
||||
&component,
|
||||
mode,
|
||||
&config,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// If we created a new components module, print instructions about the final setup steps required
|
||||
if new_components_module {
|
||||
println!(
|
||||
"Created new components module at {}.",
|
||||
components_root.display()
|
||||
);
|
||||
println!("To finish setting up components, you will need to:");
|
||||
println!("- manually reference the module by adding `mod components;` to your `main.rs` file");
|
||||
if registry.is_default() {
|
||||
println!("- add a reference to `asset!(\"/assets/dx-components-theme.css\")` as a stylesheet in your app");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the remote component registry
|
||||
Self::Update { registry } => {
|
||||
let config = Self::resolve_config().await?;
|
||||
registry
|
||||
.unwrap_or(config.components.registry.remote)
|
||||
.update()
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Remove a component from the managed component module
|
||||
Self::Remove {
|
||||
component,
|
||||
registry,
|
||||
} => {
|
||||
Self::remove_component(&component, registry).await?;
|
||||
}
|
||||
|
||||
// Clear the component registry cache
|
||||
Self::Clean => {
|
||||
_ = tokio::fs::remove_dir_all(&Workspace::component_cache_dir()).await;
|
||||
}
|
||||
|
||||
// Print the schema for component manifests
|
||||
Self::Schema => {
|
||||
let schema = component_manifest_schema();
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&schema).unwrap_or_default()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(StructuredOutput::Success)
|
||||
}
|
||||
|
||||
/// Remove a component from the managed component module
|
||||
async fn remove_component(
|
||||
component_args: &ComponentArgs,
|
||||
registry: Option<ComponentRegistry>,
|
||||
) -> Result<()> {
|
||||
let config = Self::resolve_config().await?;
|
||||
let registry = Self::resolve_registry(registry, &config)?;
|
||||
|
||||
let components_root = components_root(component_args.module_path.as_deref(), &config)?;
|
||||
|
||||
// Find the requested components
|
||||
let components = if component_args.all {
|
||||
registry
|
||||
.read_components()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|c| c.component.name)
|
||||
.collect()
|
||||
} else {
|
||||
component_args.components.clone()
|
||||
};
|
||||
|
||||
for component_name in components {
|
||||
// Remove the component module
|
||||
_ = tokio::fs::remove_dir_all(&components_root.join(&component_name)).await;
|
||||
|
||||
// Remove the module from the components mod.rs
|
||||
let mod_rs_path = components_root.join("mod.rs");
|
||||
let mod_rs_content = tokio::fs::read_to_string(&mod_rs_path)
|
||||
.await
|
||||
.with_context(|| format!("Failed to read {}", mod_rs_path.display()))?;
|
||||
let mod_line = format!("pub mod {};\n", component_name);
|
||||
let new_mod_rs_content = mod_rs_content.replace(&mod_line, "");
|
||||
tokio::fs::write(&mod_rs_path, new_mod_rs_content)
|
||||
.await
|
||||
.with_context(|| format!("Failed to write to {}", mod_rs_path.display()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load the config
|
||||
async fn resolve_config() -> Result<DioxusConfig> {
|
||||
let workspace = Workspace::current().await?;
|
||||
|
||||
let crate_package = workspace.find_main_package(None)?;
|
||||
|
||||
Ok(workspace
|
||||
.load_dioxus_config(crate_package)?
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Resolve a registry from the config if none is provided
|
||||
fn resolve_registry(
|
||||
registry: Option<ComponentRegistry>,
|
||||
config: &DioxusConfig,
|
||||
) -> Result<ComponentRegistry> {
|
||||
if let Some(registry) = registry {
|
||||
return Ok(registry);
|
||||
}
|
||||
|
||||
Ok(config.components.registry.clone())
|
||||
}
|
||||
|
||||
/// Add any rust dependencies required for a component
|
||||
async fn add_rust_dependencies(dependencies: &HashSet<CargoDependency>) -> Result<()> {
|
||||
for dep in dependencies {
|
||||
let status = Command::from(dep.add_command())
|
||||
.status()
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to run command to add dependency {} to Cargo.toml",
|
||||
dep.name()
|
||||
)
|
||||
})?;
|
||||
if !status.success() {
|
||||
bail!("Failed to add dependency {} to Cargo.toml", dep.name());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Arguments for the default or custom remote registry
|
||||
/// If both values are None, the default registry will be used
|
||||
#[derive(Clone, Debug, Parser, Default, Serialize, Deserialize)]
|
||||
pub struct RemoteComponentRegistry {
|
||||
/// The url of the the component registry
|
||||
#[arg(long)]
|
||||
git: Option<String>,
|
||||
|
||||
/// The revision of the the component registry
|
||||
#[arg(long)]
|
||||
rev: Option<String>,
|
||||
}
|
||||
|
||||
impl RemoteComponentRegistry {
|
||||
/// Resolve the path to the component registry, downloading the remote registry if needed
|
||||
async fn resolve(&self) -> Result<PathBuf> {
|
||||
// If a git url is provided use that (plus optional rev)
|
||||
// Otherwise use the built-in registry
|
||||
let (git, rev) = self.resolve_or_default();
|
||||
|
||||
let repo_dir = Workspace::component_cache_path(&git, rev.as_deref());
|
||||
|
||||
// If the repo already exists, use it otherwise clone it
|
||||
if !repo_dir.exists() {
|
||||
// If offline, we cannot download the registry
|
||||
if verbosity_or_default().offline {
|
||||
bail!("Cannot download component registry '{}' while offline", git);
|
||||
}
|
||||
|
||||
// Make sure the parent directory exists
|
||||
tokio::fs::create_dir_all(&repo_dir).await?;
|
||||
tokio::task::spawn_blocking({
|
||||
let git = git.clone();
|
||||
let repo_dir = repo_dir.clone();
|
||||
move || {
|
||||
println!("Downloading {git}...");
|
||||
|
||||
// Clone the repo
|
||||
let repo = Repository::clone(&git, repo_dir)?;
|
||||
|
||||
// If a rev is provided, checkout that rev
|
||||
if let Some(rev) = &rev {
|
||||
Self::checkout_rev(&repo, &git, rev)?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
}
|
||||
|
||||
Ok(repo_dir)
|
||||
}
|
||||
|
||||
/// Update the component registry by fetching the latest changes from the remote
|
||||
async fn update(&self) -> Result<()> {
|
||||
let (git, rev) = self.resolve_or_default();
|
||||
|
||||
// Make sure the repo is cloned
|
||||
let path = self.resolve().await?;
|
||||
|
||||
// Open the repo and update it
|
||||
tokio::task::spawn_blocking({
|
||||
let path = path.clone();
|
||||
move || {
|
||||
let repo = Repository::open(path)?;
|
||||
let mut remote = repo.find_remote("origin")?;
|
||||
// Fetch all remote branches with the same name as local branches
|
||||
remote.fetch(&["refs/heads/*:refs/heads/*"], None, None)?;
|
||||
// If a rev is provided, checkout that rev
|
||||
if let Some(rev) = &rev {
|
||||
Self::checkout_rev(&repo, &git, rev)?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// If a git url is provided use that (plus optional rev)
|
||||
/// Otherwise use the built-in registry
|
||||
fn resolve_or_default(&self) -> (String, Option<String>) {
|
||||
if let Some(git) = &self.git {
|
||||
(git.clone(), self.rev.clone())
|
||||
} else {
|
||||
("https://github.com/dioxuslabs/components".into(), None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checkout the given rev in the given repo
|
||||
fn checkout_rev(repo: &Repository, git: &str, rev: &str) -> Result<()> {
|
||||
let (object, reference) = repo
|
||||
.revparse_ext(rev)
|
||||
.with_context(|| format!("Failed to find revision '{}' in '{}'", rev, git))?;
|
||||
repo.checkout_tree(&object, None)?;
|
||||
|
||||
if let Some(gref) = reference {
|
||||
if let Some(name) = gref.name() {
|
||||
repo.set_head(name)?;
|
||||
}
|
||||
} else {
|
||||
repo.set_head_detached(object.id())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Arguments for a component registry
|
||||
/// Either a path to a local directory or a remote git repo (with optional rev)
|
||||
#[derive(Clone, Debug, Parser, Default, Serialize, Deserialize)]
|
||||
pub struct ComponentRegistry {
|
||||
/// The remote repo args
|
||||
#[clap(flatten)]
|
||||
#[serde(flatten)]
|
||||
remote: RemoteComponentRegistry,
|
||||
|
||||
/// The path to the components directory
|
||||
#[arg(long)]
|
||||
path: Option<String>,
|
||||
}
|
||||
|
||||
impl ComponentRegistry {
|
||||
/// Resolve the path to the component registry, downloading the remote registry if needed
|
||||
async fn resolve(&self) -> Result<PathBuf> {
|
||||
// If a path is provided, use that
|
||||
if let Some(path) = &self.path {
|
||||
return Ok(PathBuf::from(path));
|
||||
}
|
||||
|
||||
// Otherwise use the remote/default registry
|
||||
self.remote.resolve().await
|
||||
}
|
||||
|
||||
/// Read all components that are part of this registry
|
||||
async fn read_components(&self) -> Result<Vec<ResolvedComponent>> {
|
||||
let path = self.resolve().await?;
|
||||
|
||||
let root = read_component(&path).await?;
|
||||
let mut components = discover_components(root).await?;
|
||||
|
||||
// Filter out any virtual components with members
|
||||
components.retain(|c| c.members.is_empty());
|
||||
|
||||
Ok(components)
|
||||
}
|
||||
|
||||
/// Check if this is the default registry
|
||||
fn is_default(&self) -> bool {
|
||||
self.path.is_none() && self.remote.git.is_none() && self.remote.rev.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// A component that has been downloaded and resolved at a specific path
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
struct ResolvedComponent {
|
||||
path: PathBuf,
|
||||
component: Component,
|
||||
}
|
||||
|
||||
impl ResolvedComponent {
|
||||
/// Get the absolute paths to members of this component
|
||||
fn member_paths(&self) -> Vec<PathBuf> {
|
||||
self.component
|
||||
.members
|
||||
.iter()
|
||||
.map(|m| self.path.join(m))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ResolvedComponent {
|
||||
type Target = Component;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.component
|
||||
}
|
||||
}
|
||||
|
||||
// Find a component by name in a list of components
|
||||
fn find_component(components: &[ResolvedComponent], component: &str) -> Result<ResolvedComponent> {
|
||||
components
|
||||
.iter()
|
||||
.find(|c| c.name == component)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow::anyhow!("Component '{}' not found in registry", component))
|
||||
}
|
||||
|
||||
/// Get the path to the components module, defaulting to src/components
|
||||
fn components_root(module_path: Option<&Path>, config: &DioxusConfig) -> Result<PathBuf> {
|
||||
if let Some(module_path) = module_path {
|
||||
return Ok(PathBuf::from(module_path));
|
||||
}
|
||||
|
||||
let root = Workspace::crate_root_from_path()?;
|
||||
|
||||
if let Some(component_path) = &config.components.components_dir {
|
||||
return Ok(root.join(component_path));
|
||||
}
|
||||
|
||||
Ok(root.join("src").join("components"))
|
||||
}
|
||||
|
||||
/// Get the path to the global assets directory, defaulting to assets
|
||||
async fn global_assets_root(assets_path: Option<&Path>, config: &DioxusConfig) -> Result<PathBuf> {
|
||||
if let Some(assets_path) = assets_path {
|
||||
return Ok(PathBuf::from(assets_path));
|
||||
}
|
||||
|
||||
if let Some(asset_dir) = &config.application.asset_dir {
|
||||
return Ok(asset_dir.clone());
|
||||
}
|
||||
|
||||
let root = Workspace::crate_root_from_path()?;
|
||||
|
||||
Ok(root.join("assets"))
|
||||
}
|
||||
|
||||
/// How should we handle the component if it already exists
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum ComponentExistsBehavior {
|
||||
/// Return an error (default)
|
||||
Error,
|
||||
|
||||
/// Return early for component dependencies
|
||||
Return,
|
||||
|
||||
/// Overwrite the existing component
|
||||
Overwrite,
|
||||
}
|
||||
|
||||
/// Add a component to the managed component module
|
||||
async fn add_component(
|
||||
assets_path: Option<&Path>,
|
||||
component_path: Option<&Path>,
|
||||
component: &ResolvedComponent,
|
||||
behavior: ComponentExistsBehavior,
|
||||
config: &DioxusConfig,
|
||||
) -> Result<()> {
|
||||
// Copy the folder content to the components directory
|
||||
let components_root = components_root(component_path, config)?;
|
||||
let copied = copy_component_files(
|
||||
&component.path,
|
||||
&components_root.join(&component.name),
|
||||
&component.exclude,
|
||||
behavior,
|
||||
)
|
||||
.await?;
|
||||
if !copied {
|
||||
debug!(
|
||||
"Component '{}' already exists, skipping copy",
|
||||
component.name
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Copy any global assets
|
||||
let assets_root = global_assets_root(assets_path, config).await?;
|
||||
copy_global_assets(&assets_root, component).await?;
|
||||
|
||||
// Add the module to the components mod.rs
|
||||
let mod_rs_path = components_root.join("mod.rs");
|
||||
let mut mod_rs = tokio::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.read(true)
|
||||
.open(&mod_rs_path)
|
||||
.await
|
||||
.with_context(|| format!("Failed to open {}", mod_rs_path.display()))?;
|
||||
|
||||
// Check if the module already exists
|
||||
let mod_rs_content = tokio::fs::read_to_string(&mod_rs_path)
|
||||
.await
|
||||
.with_context(|| format!("Failed to read {}", mod_rs_path.display()))?;
|
||||
if !mod_rs_content.contains(&format!("mod {};", component.name)) {
|
||||
let mod_line = format!("pub mod {};\n", component.name);
|
||||
tokio::io::AsyncWriteExt::write_all(&mut mod_rs, mod_line.as_bytes())
|
||||
.await
|
||||
.with_context(|| format!("Failed to write to {}", mod_rs_path.display()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copy the component files. Returns true if the component was copied, false if it was skipped.
|
||||
async fn copy_component_files(
|
||||
src: &Path,
|
||||
dest: &Path,
|
||||
exclude: &[String],
|
||||
behavior: ComponentExistsBehavior,
|
||||
) -> Result<bool> {
|
||||
async fn read_dir_paths(src: &Path) -> Result<Vec<PathBuf>> {
|
||||
let mut entries = tokio::fs::read_dir(src).await?;
|
||||
let mut paths = vec![];
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
paths.push(entry.path());
|
||||
}
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
// If the directory already exists, return an error, return silently or overwrite it depending on the behavior
|
||||
if dest.exists() {
|
||||
match behavior {
|
||||
// The default behavior is to return an error
|
||||
ComponentExistsBehavior::Error => {
|
||||
bail!("Destination directory '{}' already exists", dest.display());
|
||||
}
|
||||
// For dependencies, we return early
|
||||
ComponentExistsBehavior::Return => {
|
||||
debug!(
|
||||
"Destination directory '{}' already exists, returning early",
|
||||
dest.display()
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
// If the force flag is set, we overwrite the existing component
|
||||
ComponentExistsBehavior::Overwrite => {
|
||||
debug!(
|
||||
"Destination directory '{}' already exists, overwriting",
|
||||
dest.display()
|
||||
);
|
||||
tokio::fs::remove_dir_all(dest).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokio::fs::create_dir_all(dest).await?;
|
||||
|
||||
let exclude = exclude
|
||||
.iter()
|
||||
.map(|exclude| dunce::canonicalize(src.join(exclude)))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// Set set of tasks to read directories
|
||||
let mut read_folder_tasks = JoinSet::new();
|
||||
// Set set of tasks to copy files
|
||||
let mut copy_tasks = JoinSet::new();
|
||||
|
||||
// Start by reading the source directory
|
||||
let src = src.to_path_buf();
|
||||
read_folder_tasks.spawn({
|
||||
let src = src.clone();
|
||||
async move { read_dir_paths(&src).await }
|
||||
});
|
||||
|
||||
// Continue while there are read tasks
|
||||
while let Some(res) = read_folder_tasks.join_next().await {
|
||||
let paths = res??;
|
||||
for path in paths {
|
||||
let path = dunce::canonicalize(path)?;
|
||||
|
||||
// Skip excluded paths
|
||||
if exclude.iter().any(|e| *e == path || path.starts_with(e)) {
|
||||
debug!("Excluding path {}", path.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the path in the destination directory
|
||||
let Ok(path_relative_to_src) = path.strip_prefix(&src) else {
|
||||
continue;
|
||||
};
|
||||
let dest = dest.join(path_relative_to_src);
|
||||
|
||||
// If it's a directory, read it, otherwise copy the file
|
||||
if path.is_dir() {
|
||||
read_folder_tasks.spawn(async move { read_dir_paths(&path).await });
|
||||
} else {
|
||||
copy_tasks.spawn(async move {
|
||||
if let Some(parent) = dest.parent() {
|
||||
if !parent.exists() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
}
|
||||
tokio::fs::copy(&path, &dest).await
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all copy tasks to finish
|
||||
while let Some(res) = copy_tasks.join_next().await {
|
||||
res??;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Make sure the components directory and a mod.rs file exists. Returns true if the directory was created, false if it already existed.
|
||||
async fn ensure_components_module_exists(components_dir: &Path) -> Result<bool> {
|
||||
if components_dir.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
tokio::fs::create_dir_all(&components_dir).await?;
|
||||
let mod_rs_path = components_dir.join("mod.rs");
|
||||
if mod_rs_path.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
tokio::fs::write(&mod_rs_path, "// AUTOGENERTED Components module\n").await?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Read a component from the given path
|
||||
async fn read_component(path: &Path) -> Result<ResolvedComponent> {
|
||||
let json_path = path.join("component.json");
|
||||
let bytes = tokio::fs::read(&json_path).await.with_context(|| {
|
||||
format!(
|
||||
"Failed to open component manifest at {}",
|
||||
json_path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
let component = serde_json::from_slice(&bytes)?;
|
||||
Ok(ResolvedComponent {
|
||||
path: path.to_path_buf(),
|
||||
component,
|
||||
})
|
||||
}
|
||||
|
||||
/// Recursively discover all components starting from the root component
|
||||
async fn discover_components(root: ResolvedComponent) -> Result<Vec<ResolvedComponent>> {
|
||||
// Create a queue of members to read
|
||||
let mut queue = root.member_paths();
|
||||
// The list of discovered components
|
||||
let mut components = vec![root];
|
||||
// The set of pending read tasks
|
||||
let mut pending = JoinSet::new();
|
||||
loop {
|
||||
// First, spawn tasks for all queued paths
|
||||
while let Some(root_path) = queue.pop() {
|
||||
pending.spawn(async move { read_component(&root_path).await });
|
||||
}
|
||||
// Then try to join the next task
|
||||
let Some(component) = pending.join_next().await else {
|
||||
break;
|
||||
};
|
||||
let component = component??;
|
||||
// And add the result to the queue and list
|
||||
queue.extend(component.member_paths());
|
||||
components.push(component);
|
||||
}
|
||||
Ok(components)
|
||||
}
|
||||
|
||||
/// Copy any global assets for the component
|
||||
async fn copy_global_assets(assets_root: &Path, component: &ResolvedComponent) -> Result<()> {
|
||||
let cache_dir = Workspace::component_cache_dir();
|
||||
|
||||
for path in &component.global_assets {
|
||||
let src = component.path.join(path);
|
||||
let absolute_source = dunce::canonicalize(&src).with_context(|| {
|
||||
format!(
|
||||
"Failed to find global asset '{}' for component '{}'",
|
||||
src.display(),
|
||||
component.name
|
||||
)
|
||||
})?;
|
||||
|
||||
// Make sure the source is inside the component registry somewhere
|
||||
if !absolute_source.starts_with(&cache_dir) {
|
||||
bail!(
|
||||
"Cannot copy global asset '{}' for component '{}' because it is outside of the component registry",
|
||||
src.display(),
|
||||
component.name
|
||||
);
|
||||
}
|
||||
|
||||
// Copy the file into the assets directory, preserving the file name and extension
|
||||
let dest = assets_root.join(
|
||||
absolute_source
|
||||
.components()
|
||||
.next_back()
|
||||
.context("Global assets must have at least one file component")?,
|
||||
);
|
||||
|
||||
// Make sure the asset dir exists
|
||||
if let Some(parent) = dest.parent() {
|
||||
if !parent.exists() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
}
|
||||
|
||||
tokio::fs::copy(&src, &dest).await.with_context(|| {
|
||||
format!(
|
||||
"Failed to copy global asset from {} to {}",
|
||||
src.display(),
|
||||
dest.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -250,11 +250,7 @@ pub(crate) async fn check_path(path: &std::path::PathBuf) -> Result<()> {
|
||||
/// Perform a health check against github itself before we attempt to download any templates hosted
|
||||
/// on github.
|
||||
pub(crate) async fn check_connectivity() -> Result<()> {
|
||||
if crate::VERBOSITY
|
||||
.get()
|
||||
.map(|f| f.offline)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if crate::verbosity_or_default().offline {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ pub(crate) mod build;
|
||||
pub(crate) mod build_assets;
|
||||
pub(crate) mod bundle;
|
||||
pub(crate) mod check;
|
||||
pub(crate) mod component;
|
||||
pub(crate) mod config;
|
||||
pub(crate) mod create;
|
||||
pub(crate) mod doctor;
|
||||
@@ -114,6 +115,11 @@ pub(crate) enum Commands {
|
||||
#[clap(name = "tools")]
|
||||
#[clap(subcommand)]
|
||||
Tools(BuildTools),
|
||||
|
||||
/// Run a dioxus build tool. IE `build-assets`, `hotpatch`, etc
|
||||
#[clap(name = "components")]
|
||||
#[clap(subcommand)]
|
||||
Components(component::ComponentCommand),
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
|
||||
@@ -3,6 +3,10 @@ use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct ApplicationConfig {
|
||||
/// The path where global assets will be added when components are added with `dx components add`
|
||||
#[serde(default)]
|
||||
pub(crate) asset_dir: Option<PathBuf>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) out_dir: Option<PathBuf>,
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
use crate::component::ComponentRegistry;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Configuration for the `dioxus component` commands
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub(crate) struct ComponentConfig {
|
||||
/// The component registry to default to when adding components
|
||||
#[serde(default)]
|
||||
pub(crate) registry: ComponentRegistry,
|
||||
|
||||
/// The path where components are stored when adding or removing components
|
||||
#[serde(default)]
|
||||
pub(crate) components_dir: Option<PathBuf>,
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::config::component::ComponentConfig;
|
||||
|
||||
use super::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -10,12 +12,16 @@ pub(crate) struct DioxusConfig {
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) bundle: BundleConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) components: ComponentConfig,
|
||||
}
|
||||
|
||||
impl Default for DioxusConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
application: ApplicationConfig {
|
||||
asset_dir: None,
|
||||
out_dir: None,
|
||||
public_dir: Some("public".into()),
|
||||
tailwind_input: None,
|
||||
@@ -53,6 +59,7 @@ impl Default for DioxusConfig {
|
||||
wasm_opt: Default::default(),
|
||||
},
|
||||
bundle: BundleConfig::default(),
|
||||
components: ComponentConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod app;
|
||||
mod bundle;
|
||||
mod component;
|
||||
mod dioxus_config;
|
||||
mod serve;
|
||||
mod web;
|
||||
|
||||
@@ -25,7 +25,7 @@ pub fn log_stacktrace(err: &anyhow::Error, padding: usize) -> String {
|
||||
));
|
||||
}
|
||||
|
||||
if crate::VERBOSITY.get().unwrap().trace {
|
||||
if crate::verbosity_or_default().trace {
|
||||
trace.push_str(&format!("\nBacktrace:\n{}", err.backtrace()));
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
//! - set `dx config set disable-telemetry true`
|
||||
//!
|
||||
|
||||
use crate::component::ComponentCommand;
|
||||
use crate::{dx_build_info::GIT_COMMIT_HASH_SHORT, serve::ServeUpdate, Cli, Commands, Verbosity};
|
||||
use crate::{BundleFormat, CliSettings, Workspace};
|
||||
use anyhow::{bail, Context, Error, Result};
|
||||
@@ -68,6 +69,10 @@ const DX_SRC_FLAG: &str = "dx_src";
|
||||
|
||||
pub static VERBOSITY: OnceLock<Verbosity> = OnceLock::new();
|
||||
|
||||
pub fn verbosity_or_default() -> Verbosity {
|
||||
crate::VERBOSITY.get().cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn reset_cursor() {
|
||||
use std::io::IsTerminal;
|
||||
|
||||
@@ -902,6 +907,44 @@ impl TraceController {
|
||||
Print::ClientArgs(_args) => ("print client-args".to_string(), json!({})),
|
||||
Print::ServerArgs(_args) => ("print server-args".to_string(), json!({})),
|
||||
},
|
||||
Commands::Components(cmd) => match cmd {
|
||||
ComponentCommand::Add {
|
||||
component,
|
||||
registry,
|
||||
force,
|
||||
} => (
|
||||
"components add".to_string(),
|
||||
json!({
|
||||
"component": component,
|
||||
"registry": registry,
|
||||
"force": force,
|
||||
}),
|
||||
),
|
||||
ComponentCommand::Remove {
|
||||
component,
|
||||
registry,
|
||||
} => (
|
||||
"components remove".to_string(),
|
||||
json!({
|
||||
"component": component,
|
||||
"registry": registry,
|
||||
}),
|
||||
),
|
||||
ComponentCommand::Update { registry } => (
|
||||
"components update".to_string(),
|
||||
json!({
|
||||
"registry": registry,
|
||||
}),
|
||||
),
|
||||
ComponentCommand::List { registry } => (
|
||||
"components list".to_string(),
|
||||
json!({
|
||||
"registry": registry,
|
||||
}),
|
||||
),
|
||||
ComponentCommand::Clean => ("components clean".to_string(), json!({})),
|
||||
ComponentCommand::Schema => ("components schema".to_string(), json!({})),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ async fn main() -> ExitCode {
|
||||
Commands::Tools(BuildTools::HotpatchTip(opts)) => opts.run().await,
|
||||
Commands::Doctor(opts) => opts.doctor().await,
|
||||
Commands::Print(opts) => opts.print().await,
|
||||
Commands::Components(opts) => opts.run().await,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -556,6 +556,24 @@ impl Workspace {
|
||||
pub(crate) fn global_settings_file() -> PathBuf {
|
||||
Self::dioxus_data_dir().join("settings.toml")
|
||||
}
|
||||
|
||||
/// The path where components downloaded from git are cached
|
||||
pub(crate) fn component_cache_dir() -> PathBuf {
|
||||
Self::dioxus_data_dir().join("components")
|
||||
}
|
||||
|
||||
/// Get the path to a specific component in the cache
|
||||
pub(crate) fn component_cache_path(git: &str, rev: Option<&str>) -> PathBuf {
|
||||
use std::hash::Hasher;
|
||||
|
||||
let mut hasher = std::hash::DefaultHasher::new();
|
||||
std::hash::Hash::hash(git, &mut hasher);
|
||||
if let Some(rev) = rev {
|
||||
std::hash::Hash::hash(rev, &mut hasher);
|
||||
}
|
||||
let hash = hasher.finish();
|
||||
Self::component_cache_dir().join(format!("{hash:016x}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Workspace {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "dioxus-component-manifest"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
description = "Wire format for the dioxus CLI telemetry type"
|
||||
authors = ["Evan Almloff"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react"]
|
||||
|
||||
[dependencies]
|
||||
schemars = "1.0.4"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.143"
|
||||
@@ -0,0 +1,121 @@
|
||||
use std::process::Command;
|
||||
|
||||
use schemars::{schema_for, JsonSchema, Schema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A component compatible with the dioxus components system.
|
||||
/// This may be a "virtual" component which is empty except for a list of members.
|
||||
#[derive(Deserialize, Serialize, JsonSchema, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct Component {
|
||||
pub name: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub authors: Vec<String>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub component_dependencies: Vec<ComponentDependency>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub cargo_dependencies: Vec<CargoDependency>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub members: Vec<String>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub exclude: Vec<String>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub global_assets: Vec<String>,
|
||||
}
|
||||
|
||||
/// A dependency on another component, either built-in or third-party.
|
||||
#[derive(Deserialize, Serialize, JsonSchema, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[serde(untagged)]
|
||||
pub enum ComponentDependency {
|
||||
Builtin(String),
|
||||
ThirdParty {
|
||||
name: String,
|
||||
git: String,
|
||||
#[serde(default)]
|
||||
rev: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A dependency on a cargo crate required for a component.
|
||||
#[derive(Deserialize, Serialize, JsonSchema, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[serde(untagged)]
|
||||
pub enum CargoDependency {
|
||||
Simple(String),
|
||||
Detailed {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
version: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
features: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
default_features: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
git: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
rev: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl CargoDependency {
|
||||
/// Get the `cargo add` command for this dependency.
|
||||
pub fn add_command(&self) -> Command {
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.arg("add");
|
||||
match self {
|
||||
CargoDependency::Simple(name) => {
|
||||
cmd.arg(name);
|
||||
}
|
||||
CargoDependency::Detailed {
|
||||
name,
|
||||
version,
|
||||
features,
|
||||
default_features,
|
||||
git,
|
||||
rev,
|
||||
} => {
|
||||
cmd.arg(format!(
|
||||
"{name}{}",
|
||||
version
|
||||
.as_ref()
|
||||
.map(|version| format!("@{version}"))
|
||||
.unwrap_or_default()
|
||||
));
|
||||
if !features.is_empty() {
|
||||
cmd.arg("--features").arg(features.join(","));
|
||||
}
|
||||
if !*default_features {
|
||||
cmd.arg("--no-default-features");
|
||||
}
|
||||
if let Some(git) = git {
|
||||
cmd.arg("--git").arg(git);
|
||||
}
|
||||
if let Some(rev) = rev {
|
||||
cmd.arg("--rev").arg(rev);
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Get the name of the dependency.
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
CargoDependency::Simple(name) => name,
|
||||
CargoDependency::Detailed { name, .. } => name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the JSON schema for the `Component` struct.
|
||||
pub fn component_manifest_schema() -> Schema {
|
||||
schema_for!(Component)
|
||||
}
|
||||
Reference in new issue
Block a user