1use std::collections::HashMap;
2use std::net::IpAddr;
3use std::path::PathBuf;
4use std::str::FromStr;
5use std::sync::Arc;
6
7use anyhow::{Context, Result};
8use axum::http::Uri;
9use clap::Args;
10use serde::{Deserialize, Deserializer};
11
12use crate::common::parse_public_url;
13use crate::config::{RtcBuild, RtcClean, RtcServe, RtcWatch};
14use crate::pipelines::PipelineStage;
15
16#[derive(Clone, Debug, Default, Deserialize, Args)]
18pub struct ConfigOptsBuild {
19 pub target: Option<PathBuf>,
21 #[arg(long)]
23 #[serde(default)]
24 pub release: bool,
25 #[arg(short, long)]
27 pub dist: Option<PathBuf>,
28 #[arg(long, value_parser = parse_public_url)]
30 pub public_url: Option<String>,
31 #[arg(long)]
33 #[serde(default)]
34 pub no_default_features: bool,
35 #[arg(long)]
37 #[serde(default)]
38 pub all_features: bool,
39 #[arg(long)]
42 pub features: Option<String>,
43 #[arg(long)]
45 pub filehash: Option<bool>,
46 #[arg(skip)]
54 #[serde(default)]
55 pub pattern_script: Option<String>,
56
57 #[arg(skip)]
61 #[serde(default)]
62 pub inject_scripts: Option<bool>,
63 #[arg(skip)]
71 #[serde(default)]
72 pub pattern_preload: Option<String>,
73 #[arg(skip)]
74 #[serde(default)]
75 pub pattern_params: Option<HashMap<String, String>>,
89}
90
91#[derive(Clone, Debug, Default, Deserialize, Args)]
93pub struct ConfigOptsWatch {
94 #[arg(short, long, value_name = "path")]
96 pub watch: Option<Vec<PathBuf>>,
97 #[arg(short, long, value_name = "path")]
99 pub ignore: Option<Vec<PathBuf>>,
100}
101
102#[derive(Clone, Debug, Default, Deserialize, Args)]
104pub struct ConfigOptsServe {
105 #[arg(long)]
107 pub address: Option<IpAddr>,
108 #[arg(long)]
110 pub port: Option<u16>,
111 #[arg(long)]
113 #[serde(default)]
114 pub open: bool,
115 #[arg(long = "proxy-backend")]
117 #[serde(default, deserialize_with = "deserialize_uri")]
118 pub proxy_backend: Option<Uri>,
119 #[arg(long = "proxy-rewrite")]
122 #[serde(default)]
123 pub proxy_rewrite: Option<String>,
124 #[arg(long = "proxy-ws")]
126 #[serde(default)]
127 pub proxy_ws: bool,
128 #[arg(long = "proxy-insecure")]
130 #[serde(default)]
131 pub proxy_insecure: bool,
132 #[arg(long = "no-autoreload")]
134 #[serde(default)]
135 pub no_autoreload: bool,
136}
137
138#[derive(Clone, Debug, Default, Deserialize, Args)]
140pub struct ConfigOptsClean {
141 #[arg(short, long)]
143 pub dist: Option<PathBuf>,
144 #[arg(long)]
146 #[serde(default)]
147 pub cargo: bool,
148}
149
150#[derive(Clone, Debug, Default, Deserialize)]
152pub struct ConfigOptsTools {
153 pub sass: Option<String>,
155 pub wasm_bindgen: Option<String>,
157 pub wasm_opt: Option<String>,
159 pub tailwindcss: Option<String>,
161}
162
163#[derive(Clone, Debug, Deserialize)]
169pub struct ConfigOptsProxy {
170 #[serde(deserialize_with = "deserialize_uri")]
172 pub backend: Uri,
173 pub rewrite: Option<String>,
179 #[serde(default)]
181 pub ws: bool,
182 #[serde(default)]
184 pub insecure: bool,
185}
186
187#[derive(Clone, Debug, Deserialize)]
189#[serde(rename_all = "snake_case")]
190pub struct ConfigOptsHook {
191 pub stage: PipelineStage,
193 pub command: String,
195 #[serde(default)]
197 pub command_arguments: Vec<String>,
198}
199
200fn deserialize_uri<'de, D, T>(data: D) -> std::result::Result<T, D::Error>
202where
203 D: Deserializer<'de>,
204 T: std::convert::From<Uri>,
205{
206 let val = String::deserialize(data)?;
207 Uri::from_str(val.as_str())
208 .map(Into::into)
209 .map_err(|err| serde::de::Error::custom(err.to_string()))
210}
211
212#[derive(Clone, Debug, Default, Deserialize)]
214pub struct ConfigOpts {
215 pub build: Option<ConfigOptsBuild>,
216 pub watch: Option<ConfigOptsWatch>,
217 pub serve: Option<ConfigOptsServe>,
218 pub clean: Option<ConfigOptsClean>,
219 pub tools: Option<ConfigOptsTools>,
220 pub proxy: Option<Vec<ConfigOptsProxy>>,
221 pub hooks: Option<Vec<ConfigOptsHook>>,
222}
223
224impl ConfigOpts {
225 pub fn rtc_build(cli_build: ConfigOptsBuild, config: Option<PathBuf>) -> Result<Arc<RtcBuild>> {
227 let base_layer = Self::file_and_env_layers(config)?;
228 let build_layer = Self::cli_opts_layer_build(cli_build, base_layer);
229 let build_opts = build_layer.build.unwrap_or_default();
230 let tools_opts = build_layer.tools.unwrap_or_default();
231 let hooks_opts = build_layer.hooks.unwrap_or_default();
232 Ok(Arc::new(RtcBuild::new(
233 build_opts, tools_opts, hooks_opts, false,
234 )?))
235 }
236
237 pub fn rtc_watch(
239 cli_build: ConfigOptsBuild,
240 cli_watch: ConfigOptsWatch,
241 config: Option<PathBuf>,
242 ) -> Result<Arc<RtcWatch>> {
243 let base_layer = Self::file_and_env_layers(config)?;
244 let build_layer = Self::cli_opts_layer_build(cli_build, base_layer);
245 let watch_layer = Self::cli_opts_layer_watch(cli_watch, build_layer);
246 let build_opts = watch_layer.build.unwrap_or_default();
247 let watch_opts = watch_layer.watch.unwrap_or_default();
248 let tools_opts = watch_layer.tools.unwrap_or_default();
249 let hooks_opts = watch_layer.hooks.unwrap_or_default();
250 Ok(Arc::new(RtcWatch::new(
251 build_opts, watch_opts, tools_opts, hooks_opts, false,
252 )?))
253 }
254
255 pub fn rtc_serve(
257 cli_build: ConfigOptsBuild,
258 cli_watch: ConfigOptsWatch,
259 cli_serve: ConfigOptsServe,
260 config: Option<PathBuf>,
261 ) -> Result<Arc<RtcServe>> {
262 let base_layer = Self::file_and_env_layers(config)?;
263 let build_layer = Self::cli_opts_layer_build(cli_build, base_layer);
264 let watch_layer = Self::cli_opts_layer_watch(cli_watch, build_layer);
265 let serve_layer = Self::cli_opts_layer_serve(cli_serve, watch_layer);
266 let build_opts = serve_layer.build.unwrap_or_default();
267 let watch_opts = serve_layer.watch.unwrap_or_default();
268 let serve_opts = serve_layer.serve.unwrap_or_default();
269 let tools_opts = serve_layer.tools.unwrap_or_default();
270 let hooks_opts = serve_layer.hooks.unwrap_or_default();
271 Ok(Arc::new(RtcServe::new(
272 build_opts,
273 watch_opts,
274 serve_opts,
275 tools_opts,
276 hooks_opts,
277 serve_layer.proxy,
278 )?))
279 }
280
281 pub fn rtc_clean(cli_clean: ConfigOptsClean, config: Option<PathBuf>) -> Result<Arc<RtcClean>> {
283 let base_layer = Self::file_and_env_layers(config)?;
284 let clean_layer = Self::cli_opts_layer_clean(cli_clean, base_layer);
285 let clean_opts = clean_layer.clean.unwrap_or_default();
286 Ok(Arc::new(RtcClean::new(clean_opts)))
287 }
288
289 pub fn full(config: Option<PathBuf>) -> Result<Self> {
291 Self::file_and_env_layers(config)
292 }
293
294 fn cli_opts_layer_build(cli: ConfigOptsBuild, cfg_base: Self) -> Self {
295 let opts = ConfigOptsBuild {
296 target: cli.target,
297 release: cli.release,
298 dist: cli.dist,
299 public_url: cli.public_url,
300 no_default_features: cli.no_default_features,
301 all_features: cli.all_features,
302 features: cli.features,
303 filehash: cli.filehash,
304 inject_scripts: cli.inject_scripts,
305 pattern_script: cli.pattern_script,
306 pattern_preload: cli.pattern_preload,
307 pattern_params: cli.pattern_params,
308 };
309 let cfg_build = ConfigOpts {
310 build: Some(opts),
311 watch: None,
312 serve: None,
313 clean: None,
314 tools: None,
315 proxy: None,
316 hooks: None,
317 };
318 Self::merge(cfg_base, cfg_build)
319 }
320
321 fn cli_opts_layer_watch(cli: ConfigOptsWatch, cfg_base: Self) -> Self {
322 let opts = ConfigOptsWatch {
323 watch: cli.watch,
324 ignore: cli.ignore,
325 };
326 let cfg = ConfigOpts {
327 build: None,
328 watch: Some(opts),
329 serve: None,
330 clean: None,
331 tools: None,
332 proxy: None,
333 hooks: None,
334 };
335 Self::merge(cfg_base, cfg)
336 }
337
338 fn cli_opts_layer_serve(cli: ConfigOptsServe, cfg_base: Self) -> Self {
339 let opts = ConfigOptsServe {
340 address: cli.address,
341 port: cli.port,
342 open: cli.open,
343 proxy_backend: cli.proxy_backend,
344 proxy_rewrite: cli.proxy_rewrite,
345 proxy_insecure: cli.proxy_insecure,
346 proxy_ws: cli.proxy_ws,
347 no_autoreload: cli.no_autoreload,
348 };
349 let cfg = ConfigOpts {
350 build: None,
351 watch: None,
352 serve: Some(opts),
353 clean: None,
354 tools: None,
355 proxy: None,
356 hooks: None,
357 };
358 Self::merge(cfg_base, cfg)
359 }
360
361 fn cli_opts_layer_clean(cli: ConfigOptsClean, cfg_base: Self) -> Self {
362 let opts = ConfigOptsClean {
363 dist: cli.dist,
364 cargo: cli.cargo,
365 };
366 let cfg = ConfigOpts {
367 build: None,
368 watch: None,
369 serve: None,
370 clean: Some(opts),
371 tools: None,
372 proxy: None,
373 hooks: None,
374 };
375 Self::merge(cfg_base, cfg)
376 }
377
378 fn file_and_env_layers(path: Option<PathBuf>) -> Result<Self> {
379 let toml_cfg = Self::from_file(path)?;
380 let env_cfg = Self::from_env().context("error reading trunk env var config")?;
381 let cfg = Self::merge(toml_cfg, env_cfg);
382 Ok(cfg)
383 }
384
385 fn from_file(path: Option<PathBuf>) -> Result<Self> {
390 let mut trunk_toml_path = path.unwrap_or_else(|| "Trunk.toml".into());
391 if !trunk_toml_path.exists() {
392 return Ok(Default::default());
393 }
394 if !trunk_toml_path.is_absolute() {
395 trunk_toml_path = trunk_toml_path.canonicalize().with_context(|| {
396 format!(
397 "error getting canonical path to Trunk config file {:?}",
398 &trunk_toml_path
399 )
400 })?;
401 }
402 let cfg_bytes =
403 std::fs::read_to_string(&trunk_toml_path).context("error reading config file")?;
404 let mut cfg: Self = toml::from_str(&cfg_bytes)
405 .context("error reading config file contents as TOML data")?;
406 if let Some(parent) = trunk_toml_path.parent() {
407 if let Some(build) = cfg.build.as_mut() {
408 if let Some(target) = build.target.as_mut() {
409 if !target.is_absolute() {
410 *target =
411 std::fs::canonicalize(parent.join(&target)).with_context(|| {
412 format!(
413 "error taking canonical path to [build].target {:?} in {:?}",
414 target, trunk_toml_path
415 )
416 })?;
417 }
418 }
419 if let Some(dist) = build.dist.as_mut() {
420 if !dist.is_absolute() {
421 *dist = parent.join(&dist);
422 }
423 }
424 }
425 if let Some(watch) = cfg.watch.as_mut() {
426 if let Some(watch_paths) = watch.watch.as_mut() {
427 for path in watch_paths.iter_mut() {
428 if !path.is_absolute() {
429 *path =
430 std::fs::canonicalize(parent.join(&path)).with_context(|| {
431 format!(
432 "error taking canonical path to [watch].watch {:?} in {:?}",
433 path, trunk_toml_path
434 )
435 })?;
436 }
437 }
438 }
439 if let Some(ignore_paths) = watch.ignore.as_mut() {
440 for path in ignore_paths.iter_mut() {
441 if !path.is_absolute() {
442 *path =
443 std::fs::canonicalize(parent.join(&path)).with_context(|| {
444 format!(
445 "error taking canonical path to [watch].ignore {:?} in \
446 {:?}",
447 path, trunk_toml_path
448 )
449 })?;
450 }
451 }
452 }
453 }
454 if let Some(clean) = cfg.clean.as_mut() {
455 if let Some(dist) = clean.dist.as_mut() {
456 if !dist.is_absolute() {
457 *dist = parent.join(&dist);
458 }
459 }
460 }
461 }
462 Ok(cfg)
463 }
464
465 fn from_env() -> Result<Self> {
466 Ok(ConfigOpts {
467 build: Some(envy::prefixed("TRUNK_BUILD_").from_env()?),
468 watch: Some(envy::prefixed("TRUNK_WATCH_").from_env()?),
469 serve: Some(envy::prefixed("TRUNK_SERVE_").from_env()?),
470 clean: Some(envy::prefixed("TRUNK_CLEAN_").from_env()?),
471 tools: Some(envy::prefixed("TRUNK_TOOLS_").from_env()?),
472 proxy: None,
473 hooks: None,
474 })
475 }
476
477 fn merge(mut lesser: Self, mut greater: Self) -> Self {
479 greater.build = match (lesser.build.take(), greater.build.take()) {
480 (None, None) => None,
481 (Some(val), None) | (None, Some(val)) => Some(val),
482 (Some(l), Some(mut g)) => {
483 g.target = g.target.or(l.target);
484 g.dist = g.dist.or(l.dist);
485 g.public_url = g.public_url.or(l.public_url);
486 g.filehash = g.filehash.or(l.filehash);
487 if l.release {
489 g.release = true;
490 }
491 g.inject_scripts = g.inject_scripts.or(l.inject_scripts);
492 g.pattern_preload = g.pattern_preload.or(l.pattern_preload);
493 g.pattern_script = g.pattern_script.or(l.pattern_script);
494 g.pattern_params = g.pattern_params.or(l.pattern_params);
495 Some(g)
496 }
497 };
498 greater.watch = match (lesser.watch.take(), greater.watch.take()) {
499 (None, None) => None,
500 (Some(val), None) | (None, Some(val)) => Some(val),
501 (Some(l), Some(mut g)) => {
502 g.watch = g.watch.or(l.watch);
503 g.ignore = g.ignore.or(l.ignore);
504 Some(g)
505 }
506 };
507 greater.serve = match (lesser.serve.take(), greater.serve.take()) {
508 (None, None) => None,
509 (Some(val), None) | (None, Some(val)) => Some(val),
510 (Some(l), Some(mut g)) => {
511 g.proxy_backend = g.proxy_backend.or(l.proxy_backend);
512 g.proxy_rewrite = g.proxy_rewrite.or(l.proxy_rewrite);
513 g.address = g.address.or(l.address);
514 g.port = g.port.or(l.port);
515 g.proxy_ws = g.proxy_ws || l.proxy_ws;
516 if l.no_autoreload {
518 g.no_autoreload = true;
519 }
520 if l.open {
522 g.open = true;
523 }
524 Some(g)
525 }
526 };
527 greater.tools = match (lesser.tools.take(), greater.tools.take()) {
528 (None, None) => None,
529 (Some(val), None) | (None, Some(val)) => Some(val),
530 (Some(l), Some(mut g)) => {
531 g.sass = g.sass.or(l.sass);
532 g.wasm_bindgen = g.wasm_bindgen.or(l.wasm_bindgen);
533 g.wasm_opt = g.wasm_opt.or(l.wasm_opt);
534 Some(g)
535 }
536 };
537 greater.clean = match (lesser.clean.take(), greater.clean.take()) {
538 (None, None) => None,
539 (Some(val), None) | (None, Some(val)) => Some(val),
540 (Some(l), Some(mut g)) => {
541 g.dist = g.dist.or(l.dist);
542 if l.cargo {
544 g.cargo = true;
545 }
546 Some(g)
547 }
548 };
549 greater.proxy = match (lesser.proxy.take(), greater.proxy.take()) {
550 (None, None) => None,
551 (Some(val), None) | (None, Some(val)) => Some(val),
552 (Some(_), Some(g)) => Some(g), };
554 greater.hooks = match (lesser.hooks.take(), greater.hooks.take()) {
555 (None, None) => None,
556 (Some(val), None) | (None, Some(val)) => Some(val),
557 (Some(_), Some(g)) => Some(g), };
559 greater
560 }
561}