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)]
27pub struct BuildPluginArgs {
28 #[arg(default_value = ".")]
30 pub path: PathBuf,
31
32 #[arg(long)]
34 pub install: bool,
35
36 #[arg(long)]
38 pub release: bool,
39}
40
41#[derive(Deserialize)]
43struct MinimalManifest {
44 name: Option<String>,
45 server: Option<ServerSection>,
46 web: Option<WebSection>,
47}
48
49#[derive(Deserialize)]
50struct ServerSection {
51 entry: String,
52}
53
54#[derive(Deserialize)]
55struct WebSection {
56 root: String,
57 #[allow(dead_code)]
58 entry: String,
59}
60
61pub fn run(args: BuildPluginArgs) -> anyhow::Result<()> {
62 let plugin_dir = args
63 .path
64 .canonicalize()
65 .with_context(|| format!("Cannot find directory '{}'", args.path.display()))?;
66
67 let manifest_path = plugin_dir.join("plugin.toml");
68 if !manifest_path.exists() {
69 bail!(
70 "Not a broccoli plugin directory: no plugin.toml found in '{}'.\n\
71 Run `broccoli plugin new` to create a new plugin.",
72 plugin_dir.display()
73 );
74 }
75
76 let manifest_content =
77 std::fs::read_to_string(&manifest_path).context("Failed to read plugin.toml")?;
78 let manifest: MinimalManifest =
79 toml::from_str(&manifest_content).context("Failed to parse plugin.toml")?;
80
81 let plugin_name = manifest.name.as_deref().unwrap_or("plugin");
82 let mut built_anything = false;
83
84 if let Some(server) = manifest.server.as_ref() {
86 println!(
87 "{} Building backend for {}...",
88 style("→").blue().bold(),
89 style(plugin_name).cyan()
90 );
91
92 let mut cargo_args = vec!["build", "--target", "wasm32-wasip1"];
93 if args.release {
94 cargo_args.push("--release");
95 }
96
97 let status = Command::new("cargo")
98 .args(&cargo_args)
99 .current_dir(&plugin_dir)
100 .status()
101 .context("Failed to run cargo build. Is Rust installed?")?;
102
103 if !status.success() {
104 bail!("Backend build failed");
105 }
106
107 copy_wasm_artifact(&plugin_dir, &server.entry, args.release)?;
108
109 println!("{} Backend build complete", style("✓").green().bold());
110 built_anything = true;
111 }
112
113 if manifest.web.is_some() {
115 let web_root = manifest.web.as_ref().map(|w| w.root.as_str());
116 let dev = dev_config::resolve(&plugin_dir, web_root);
117
118 let fe_dir = dev.frontend_dir.unwrap_or_else(|| plugin_dir.clone());
119
120 if !fe_dir.exists() {
121 bail!(
122 "Frontend directory '{}' does not exist.\n\
123 Check build.frontend_dir in broccoli.dev.toml.",
124 fe_dir.display()
125 );
126 }
127
128 let node_modules_exists = fe_dir.join("node_modules").exists();
130 if !node_modules_exists || args.install {
131 let install_cmd_str = dev.frontend_install_cmd.join(" ");
132
133 if args.install {
134 println!(
135 "{} Running '{}' in {}...",
136 style("→").blue().bold(),
137 style(&install_cmd_str).cyan(),
138 fe_dir.display()
139 );
140 } else {
141 println!(
142 "{} node_modules not found. Auto-running '{}'...",
143 style("!").yellow().bold(),
144 style(&install_cmd_str).cyan()
145 );
146 }
147
148 let (program, cmd_args) = dev
149 .frontend_install_cmd
150 .split_first()
151 .context("frontend_install_cmd is empty in broccoli.dev.toml")?;
152
153 let status = Command::new(program)
154 .args(cmd_args)
155 .current_dir(&fe_dir)
156 .status()
157 .with_context(|| format!("Failed to run '{}'", install_cmd_str))?;
158
159 if !status.success() {
160 bail!("Frontend installation failed");
161 }
162 println!("{} Dependencies installed", style("✓").green().bold());
163 }
164
165 println!(
166 "{} Building frontend for {}...",
167 style("→").blue().bold(),
168 style(plugin_name).cyan()
169 );
170
171 let (program, cmd_args) = dev
172 .frontend_build_cmd
173 .split_first()
174 .context("frontend_build_cmd is empty in broccoli.dev.toml")?;
175
176 let status = Command::new(program)
177 .args(cmd_args)
178 .current_dir(&fe_dir)
179 .status()
180 .with_context(|| {
181 format!(
182 "Failed to run '{}'. Is it installed?",
183 dev.frontend_build_cmd.join(" ")
184 )
185 })?;
186
187 if !status.success() {
188 bail!("Frontend build failed");
189 }
190
191 println!("{} Frontend build complete", style("✓").green().bold());
192 built_anything = true;
193 }
194
195 if !built_anything {
196 println!(
197 "{} plugin.toml has no [server] or [web] section — nothing to build.",
198 style("!").yellow().bold()
199 );
200 }
201
202 Ok(())
203}