Skip to main content

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, utils::Git};
8use foundry_common::compile::ProjectCompiler;
9use foundry_config::{Config, load_config_with_root};
10use std::path::PathBuf;
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        // Attempt to read repo URL from git
82        if doc_config.repository.is_none()
83            && let Some(remote) = Git::new(root).remote_url("origin")
84            && let Some(captures) = GH_REPO_PREFIX_REGEX.captures(&remote)
85        {
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 =
90                Some(format!("https://{brand}.{tld}/{}", project.trim_end_matches(".git")));
91        }
92
93        let commit = Git::new(root).commit_hash(false, "HEAD").ok();
94
95        let mut builder = DocBuilder::new(
96            root.clone(),
97            project.paths.sources,
98            project.paths.libraries,
99            self.include_libraries,
100        )
101        .with_should_build(self.build)
102        .with_config(doc_config.clone())
103        .with_fmt(config.fmt)
104        .with_preprocessor(ContractInheritance { include_libraries: self.include_libraries })
105        .with_preprocessor(Inheritdoc::default())
106        .with_preprocessor(InferInlineHyperlinks::default())
107        .with_preprocessor(GitSource {
108            root: root.clone(),
109            commit,
110            repository: doc_config.repository.clone(),
111        });
112
113        // If deployment docgen is enabled, add the [Deployments] preprocessor
114        if let Some(deployments) = self.deployments {
115            builder = builder.with_preprocessor(Deployments { root: root.clone(), deployments });
116        }
117
118        builder.build(compiler)?;
119
120        if self.serve {
121            Server::new(doc_config.out)
122                .with_hostname(self.hostname.unwrap_or_else(|| "localhost".into()))
123                .with_port(self.port.unwrap_or(3000))
124                .open(self.open)
125                .serve()?;
126        }
127
128        Ok(())
129    }
130
131    /// Returns whether watch mode is enabled
132    pub const fn is_watch(&self) -> bool {
133        self.watch.watch.is_some()
134    }
135
136    pub fn config(&self) -> Result<Config> {
137        load_config_with_root(self.root.as_deref())
138    }
139}