broccoli_cli/commands/plugin/
build.rs1use std::path::PathBuf;
2use std::process::Command;
3
4use anyhow::{Context, bail};
5use clap::Args;
6use console::style;
7use serde::Deserialize;
8
9use crate::dev_config;
10
11use super::wasm::copy_wasm_artifact;
12
13#[derive(Args)]
26pub struct BuildPluginArgs {
27 #[arg(default_value = ".")]
29 pub path: PathBuf,
30
31 #[arg(long)]
33 pub release: bool,
34}
35
36#[derive(Deserialize)]
38struct MinimalManifest {
39 name: Option<String>,
40 server: Option<ServerSection>,
41 web: Option<WebSection>,
42}
43
44#[derive(Deserialize)]
45struct ServerSection {
46 entry: String,
47}
48
49#[derive(Deserialize)]
50struct WebSection {
51 root: String,
52 #[allow(dead_code)]
53 entry: String,
54}
55
56pub fn run(args: BuildPluginArgs) -> anyhow::Result<()> {
57 let plugin_dir = args
58 .path
59 .canonicalize()
60 .with_context(|| format!("Cannot find directory '{}'", args.path.display()))?;
61
62 let manifest_path = plugin_dir.join("plugin.toml");
63 if !manifest_path.exists() {
64 bail!(
65 "Not a broccoli plugin directory: no plugin.toml found in '{}'.\n\
66 Run `broccoli plugin new` to create a new plugin.",
67 plugin_dir.display()
68 );
69 }
70
71 let manifest_content =
72 std::fs::read_to_string(&manifest_path).context("Failed to read plugin.toml")?;
73 let manifest: MinimalManifest =
74 toml::from_str(&manifest_content).context("Failed to parse plugin.toml")?;
75
76 let plugin_name = manifest.name.as_deref().unwrap_or("plugin");
77 let mut built_anything = false;
78
79 if let Some(server) = manifest.server.as_ref() {
81 println!(
82 "{} Building backend for {}...",
83 style("→").blue().bold(),
84 style(plugin_name).cyan()
85 );
86
87 let mut cargo_args = vec!["build", "--target", "wasm32-wasip1"];
88 if args.release {
89 cargo_args.push("--release");
90 }
91
92 let status = Command::new("cargo")
93 .args(&cargo_args)
94 .current_dir(&plugin_dir)
95 .status()
96 .context("Failed to run cargo build. Is Rust installed?")?;
97
98 if !status.success() {
99 bail!("Backend build failed");
100 }
101
102 copy_wasm_artifact(&plugin_dir, &server.entry, args.release)?;
103
104 println!("{} Backend build complete", style("✓").green().bold());
105 built_anything = true;
106 }
107
108 if manifest.web.is_some() {
110 let web_root = manifest.web.as_ref().map(|w| w.root.as_str());
111 let dev = dev_config::resolve(&plugin_dir, web_root);
112
113 let fe_dir = dev.frontend_dir.unwrap_or_else(|| plugin_dir.clone());
114
115 if !fe_dir.exists() {
116 bail!(
117 "Frontend directory '{}' does not exist.\n\
118 Check build.frontend_dir in broccoli.dev.toml.",
119 fe_dir.display()
120 );
121 }
122
123 println!(
124 "{} Building frontend for {}...",
125 style("→").blue().bold(),
126 style(plugin_name).cyan()
127 );
128
129 let (program, cmd_args) = dev
130 .frontend_cmd
131 .split_first()
132 .context("frontend_cmd is empty in broccoli.dev.toml")?;
133
134 let status = Command::new(program)
135 .args(cmd_args)
136 .current_dir(&fe_dir)
137 .status()
138 .with_context(|| {
139 format!(
140 "Failed to run '{}'. Is it installed?",
141 dev.frontend_cmd.join(" ")
142 )
143 })?;
144
145 if !status.success() {
146 bail!("Frontend build failed");
147 }
148
149 println!("{} Frontend build complete", style("✓").green().bold());
150 built_anything = true;
151 }
152
153 if !built_anything {
154 println!(
155 "{} plugin.toml has no [server] or [web] section — nothing to build.",
156 style("!").yellow().bold()
157 );
158 }
159
160 Ok(())
161}