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