Skip to main content

dmc/engine/
config.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4use crate::engine::{collection::Collection, compile::CompileConfig};
5
6/// Top-level engine config. Drives `Engine::run`: collections, output
7/// location, schema strictness, JS plugin hooks (remark/rehype via the
8/// Node sidecar), and feature flags such as GFM toggling.
9#[derive(Deserialize, Serialize, Clone)]
10#[serde(default)]
11pub struct EngineConfig {
12  pub root: PathBuf,
13  pub output_dir: PathBuf,
14  pub output_name: Option<String>,
15  pub output_format: Option<String>,
16  pub clean: bool,
17  pub strict: bool,
18  pub collections: Vec<Collection>,
19  pub include_html: bool,
20  /// Persist per-file compile output to `<output_dir>/.cache/dmc/`. On
21  /// the next build, files whose source bytes + config are unchanged
22  /// skip lex/parse/transform/codegen + sidecar entirely.
23  pub cache_enabled: bool,
24
25  #[serde(flatten)]
26  pub compile: CompileConfig,
27}
28
29impl Default for EngineConfig {
30  fn default() -> Self {
31    Self {
32      root: PathBuf::new(),
33      output_dir: PathBuf::new(),
34      output_name: None,
35      output_format: None,
36      clean: false,
37      strict: false,
38      collections: Vec::new(),
39      include_html: false,
40      cache_enabled: true,
41      compile: CompileConfig::default(),
42    }
43  }
44}
45
46impl EngineConfig {
47  /// Read `dmc.toml` (or a `.ts` / `.js` / `.mjs` config) into an
48  /// `EngineConfig`. Routes through `load_ts` for JS-flavoured configs.
49  pub(crate) fn load(config_path: &PathBuf) -> std::io::Result<EngineConfig> {
50    let ext = config_path.extension().and_then(|s| s.to_str()).unwrap_or("");
51    if matches!(ext, "ts" | "js" | "mjs") {
52      return Self::load_ts(config_path);
53    }
54    let raw = std::fs::read_to_string(config_path)?;
55    let cfg: EngineConfig =
56      toml::from_str(&raw).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
57
58    Ok(cfg)
59  }
60
61  /// Spawn a Node sidecar that imports the user's TS/JS config and prints
62  /// the resolved `EngineConfig` as JSON. Lets configs reference JS plugins
63  /// (remark / rehype) and runtime helpers.
64  fn load_ts(config: &PathBuf) -> std::io::Result<EngineConfig> {
65    use std::io::Write;
66    let abs = std::fs::canonicalize(config)?;
67    let script = include_str!("../../scripts/load-config.mjs");
68    let mut tmp = tempfile::Builder::new().suffix(".mjs").tempfile()?;
69    tmp.write_all(script.as_bytes())?;
70    tmp.flush()?;
71    let tmp_path = tmp.path().to_path_buf();
72
73    let attempts: &[(&str, &[&str])] = &[("bun", &[]), ("node", &["--import", "tsx"])];
74    let mut last_err: Option<String> = None;
75    for (cmd, prefix_args) in attempts {
76      let mut c = std::process::Command::new(cmd);
77      c.args(*prefix_args).arg(&tmp_path).arg(&abs);
78      match c.output() {
79        Ok(out) if out.status.success() => {
80          let json = String::from_utf8(out.stdout)
81            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
82          let cfg: EngineConfig = serde_json::from_str(&json).map_err(|e| {
83            std::io::Error::new(std::io::ErrorKind::InvalidData, format!("ts config: {e}\n--- output ---\n{json}"))
84          })?;
85          return Ok(cfg);
86        },
87        Ok(out) => {
88          last_err = Some(format!("{cmd} exit {}: {}", out.status, String::from_utf8_lossy(&out.stderr)));
89        },
90        Err(e) => last_err = Some(format!("{cmd}: {e}")),
91      }
92    }
93    Err(std::io::Error::new(
94      std::io::ErrorKind::NotFound,
95      format!("ts config requires `bun` or `node` w/ tsx on PATH ({})", last_err.unwrap_or_default(),),
96    ))
97  }
98}