1use crate::cmd::{install, watch::WatchArgs};
4use clap::{Parser, ValueHint};
5use eyre::Result;
6use forge_doc::DocBuilder;
7use foundry_cli::{
8 opts::{GH_REPO_PREFIX_REGEX, configure_pcx_all_sources_with_status},
9 utils::Git,
10};
11use foundry_common::{compile::ProjectCompiler, shell};
12use foundry_config::{Config, load_config_with_root};
13use solar::{interface::Session, sema::Compiler};
14use std::{path::PathBuf, time::Instant};
15
16#[derive(Clone, Debug, Parser)]
17pub struct DocArgs {
18 #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
23 pub root: Option<PathBuf>,
24
25 #[arg(long, short, value_hint = ValueHint::DirPath, value_name = "PATH")]
32 out: Option<PathBuf>,
33
34 #[arg(long, short)]
36 pub include_libraries: bool,
37
38 #[arg(long, value_name = "PATH", num_args(0..=1))]
43 pub deployments: Option<Option<PathBuf>>,
44
45 #[command(flatten)]
46 pub watch: WatchArgs,
47
48 #[arg(long, hide = true)]
50 serve: bool,
51}
52
53impl DocArgs {
54 pub async fn run(self) -> Result<()> {
55 if self.serve {
56 eyre::bail!(
57 "`--serve` has been removed. Generate the docs with `forge doc`, \
58 then run `npm run dev` from the generated docs directory."
59 );
60 }
61 let mut config = self.config()?;
62
63 install::install_missing_dependencies(&mut config, || self.config())?;
64
65 let root = &config.root;
66 let project = config.ephemeral_project()?;
67 let mut compiler = Compiler::new(Session::builder().with_stderr_emitter().build());
68 let source_status = compiler.enter_mut(|compiler| -> Result<_> {
69 let mut pcx = compiler.parse();
70 pcx.set_resolve_imports(true);
71 let status =
72 configure_pcx_all_sources_with_status(&mut pcx, &config, Some(&project), None)?;
73 pcx.parse();
74 Ok(status)
75 })?;
76
77 let mut output = if source_status.is_fully_supported() {
80 None
81 } else {
82 let mut compile_project = config.solar_project()?;
83 compile_project.no_artifacts = true;
84 Some(ProjectCompiler::new().compile(&compile_project)?)
85 };
86
87 let mut doc_cfg = config.doc;
88 if let Some(out) = self.out.clone() {
89 doc_cfg.out = out;
90 }
91
92 if doc_cfg.repository.is_none()
94 && let Some(remote) = Git::new(root).remote_url("origin")
95 && let Some(captures) = GH_REPO_PREFIX_REGEX.captures(&remote)
96 {
97 let brand = captures.name("brand").unwrap().as_str();
98 let tld = captures.name("tld").unwrap().as_str();
99 let project_path = GH_REPO_PREFIX_REGEX.replace(&remote, "");
100 doc_cfg.repository =
101 Some(format!("https://{brand}.{tld}/{}", project_path.trim_end_matches(".git")));
102 }
103
104 let git = Git::new(root);
105 let commit = doc_cfg.commit.clone().or_else(|| git.commit_hash(false, "HEAD").ok());
106 let branch = git.current_rev_branch(root).ok().map(|(_, b)| b).filter(|b| b != "HEAD");
109
110 let mut builder = DocBuilder::new(
111 root.clone(),
112 project.paths.sources,
113 project.paths.libraries,
114 self.include_libraries,
115 );
116 builder.commit = commit;
117 builder.branch = branch;
118 builder.deployments = self.deployments;
119 builder.config = doc_cfg.clone();
120
121 let out_dir = if doc_cfg.out.is_absolute() { doc_cfg.out } else { root.join(&doc_cfg.out) };
122
123 if !shell::is_quiet() {
124 sh_println!("Generating documentation...")?;
125 }
126 let started = Instant::now();
127 let compiler = if let Some(output) = &mut output {
128 output.parser_mut().solc_mut().compiler_mut()
129 } else {
130 &mut compiler
131 };
132 let stats = builder.build(compiler)?;
133 let elapsed = started.elapsed();
134
135 if !shell::is_quiet() {
136 sh_println!(
137 "Generated {pages} page{ps} from {sources} source{ss} in {elapsed:.2?} (render {render:.2?}, site {site:.2?})",
138 pages = stats.pages,
139 ps = if stats.pages <= 1 { "" } else { "s" },
140 sources = stats.sources,
141 ss = if stats.sources <= 1 { "" } else { "s" },
142 elapsed = elapsed,
143 render = stats.render_elapsed,
144 site = stats.site_elapsed,
145 )?;
146
147 sh_println!(
150 "\nDocumentation written to: {}\n\nTo preview:\n cd {}\n npm install --legacy-peer-deps\n npm run dev",
151 out_dir.display(),
152 out_dir.display(),
153 )?;
154 }
155
156 Ok(())
157 }
158
159 pub const fn is_watch(&self) -> bool {
161 self.watch.watch.is_some()
162 }
163
164 pub fn config(&self) -> Result<Config> {
165 load_config_with_root(self.root.as_deref())
166 }
167}