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::{load_config_with_root, Config};
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 _output = compiler.compile(&project)?;
75
76        let mut doc_config = config.doc;
77        if let Some(out) = self.out {
78            doc_config.out = out;
79        }
80        if doc_config.repository.is_none() {
81            // Attempt to read repo from git
82            if let Ok(output) = Command::new("git").args(["remote", "get-url", "origin"]).output() {
83                if !output.stdout.is_empty() {
84                    let remote = String::from_utf8(output.stdout)?.trim().to_owned();
85                    if let Some(captures) = GH_REPO_PREFIX_REGEX.captures(&remote) {
86                        let brand = captures.name("brand").unwrap().as_str();
87                        let tld = captures.name("tld").unwrap().as_str();
88                        let project = GH_REPO_PREFIX_REGEX.replace(&remote, "");
89                        doc_config.repository = Some(format!(
90                            "https://{brand}.{tld}/{}",
91                            project.trim_end_matches(".git")
92                        ));
93                    }
94                }
95            }
96        }
97
98        let commit = foundry_cli::utils::Git::new(root).commit_hash(false, "HEAD").ok();
99
100        let mut builder = DocBuilder::new(
101            root.clone(),
102            project.paths.sources,
103            project.paths.libraries,
104            self.include_libraries,
105        )
106        .with_should_build(self.build)
107        .with_config(doc_config.clone())
108        .with_fmt(config.fmt)
109        .with_preprocessor(ContractInheritance { include_libraries: self.include_libraries })
110        .with_preprocessor(Inheritdoc::default())
111        .with_preprocessor(InferInlineHyperlinks::default())
112        .with_preprocessor(GitSource {
113            root: root.clone(),
114            commit,
115            repository: doc_config.repository.clone(),
116        });
117
118        // If deployment docgen is enabled, add the [Deployments] preprocessor
119        if let Some(deployments) = self.deployments {
120            builder = builder.with_preprocessor(Deployments { root: root.clone(), deployments });
121        }
122
123        builder.build()?;
124
125        if self.serve {
126            Server::new(doc_config.out)
127                .with_hostname(self.hostname.unwrap_or_else(|| "localhost".into()))
128                .with_port(self.port.unwrap_or(3000))
129                .open(self.open)
130                .serve()?;
131        }
132
133        Ok(())
134    }
135
136    /// Returns whether watch mode is enabled
137    pub fn is_watch(&self) -> bool {
138        self.watch.watch.is_some()
139    }
140
141    pub fn config(&self) -> Result<Config> {
142        load_config_with_root(self.root.as_deref())
143    }
144}