forge/cmd/doc/
mod.rs

1use super::watch::WatchArgs;
2use clap::{Parser, ValueHint};
3use eyre::Result;
4use forge_doc::{
5    ContractInheritance, Deployments, DocBuilder, GitSource, InferInlineHyperlinks, Inheritdoc,
6};
7use foundry_cli::opts::GH_REPO_PREFIX_REGEX;
8use foundry_common::compile::ProjectCompiler;
9use foundry_config::{Config, load_config_with_root};
10use std::{path::PathBuf, process::Command};
11
12mod server;
13use server::Server;
14
15#[derive(Clone, Debug, Parser)]
16pub struct DocArgs {
17    /// The project's root path.
18    ///
19    /// By default root of the Git repository, if in one,
20    /// or the current working directory.
21    #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
22    pub root: Option<PathBuf>,
23
24    /// The doc's output path.
25    ///
26    /// By default, it is the `docs/` in project root.
27    #[arg(
28        long,
29        short,
30        value_hint = ValueHint::DirPath,
31        value_name = "PATH",
32    )]
33    out: Option<PathBuf>,
34
35    /// Build the `mdbook` from generated files.
36    #[arg(long, short)]
37    build: bool,
38
39    /// Serve the documentation.
40    #[arg(long, short)]
41    serve: bool,
42
43    /// Open the documentation in a browser after serving.
44    #[arg(long, requires = "serve")]
45    open: bool,
46
47    /// Hostname for serving documentation.
48    #[arg(long, requires = "serve")]
49    hostname: Option<String>,
50
51    #[command(flatten)]
52    pub watch: WatchArgs,
53
54    /// Port for serving documentation.
55    #[arg(long, short, requires = "serve")]
56    port: Option<usize>,
57
58    /// The relative path to the `hardhat-deploy` or `forge-deploy` artifact directory. Leave blank
59    /// for default.
60    #[arg(long)]
61    deployments: Option<Option<PathBuf>>,
62
63    /// Whether to create docs for external libraries.
64    #[arg(long, short)]
65    include_libraries: bool,
66}
67
68impl DocArgs {
69    pub async fn run(self) -> Result<()> {
70        let config = self.config()?;
71        let root = &config.root;
72        let project = config.project()?;
73        let compiler = ProjectCompiler::new().quiet(true);
74        let mut output = compiler.compile(&project)?;
75        let compiler = output.parser_mut().solc_mut().compiler_mut();
76
77        let mut doc_config = config.doc;
78        if let Some(out) = self.out {
79            doc_config.out = out;
80        }
81        if doc_config.repository.is_none() {
82            // Attempt to read repo from git
83            if let Ok(output) = Command::new("git").args(["remote", "get-url", "origin"]).output()
84                && !output.stdout.is_empty()
85            {
86                let remote = String::from_utf8(output.stdout)?.trim().to_owned();
87                if let Some(captures) = GH_REPO_PREFIX_REGEX.captures(&remote) {
88                    let brand = captures.name("brand").unwrap().as_str();
89                    let tld = captures.name("tld").unwrap().as_str();
90                    let project = GH_REPO_PREFIX_REGEX.replace(&remote, "");
91                    doc_config.repository =
92                        Some(format!("https://{brand}.{tld}/{}", project.trim_end_matches(".git")));
93                }
94            }
95        }
96
97        let commit = foundry_cli::utils::Git::new(root).commit_hash(false, "HEAD").ok();
98
99        let mut builder = DocBuilder::new(
100            root.clone(),
101            project.paths.sources,
102            project.paths.libraries,
103            self.include_libraries,
104        )
105        .with_should_build(self.build)
106        .with_config(doc_config.clone())
107        .with_fmt(config.fmt)
108        .with_preprocessor(ContractInheritance { include_libraries: self.include_libraries })
109        .with_preprocessor(Inheritdoc::default())
110        .with_preprocessor(InferInlineHyperlinks::default())
111        .with_preprocessor(GitSource {
112            root: root.clone(),
113            commit,
114            repository: doc_config.repository.clone(),
115        });
116
117        // If deployment docgen is enabled, add the [Deployments] preprocessor
118        if let Some(deployments) = self.deployments {
119            builder = builder.with_preprocessor(Deployments { root: root.clone(), deployments });
120        }
121
122        builder.build(compiler)?;
123
124        if self.serve {
125            Server::new(doc_config.out)
126                .with_hostname(self.hostname.unwrap_or_else(|| "localhost".into()))
127                .with_port(self.port.unwrap_or(3000))
128                .open(self.open)
129                .serve()?;
130        }
131
132        Ok(())
133    }
134
135    /// Returns whether watch mode is enabled
136    pub fn is_watch(&self) -> bool {
137        self.watch.watch.is_some()
138    }
139
140    pub fn config(&self) -> Result<Config> {
141        load_config_with_root(self.root.as_deref())
142    }
143}