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 if install::install_missing_dependencies(&mut config).await && config.auto_detect_remappings
64 {
65 config = self.config()?;
67 }
68
69 let root = &config.root;
70 let project = config.ephemeral_project()?;
71 let mut compiler = Compiler::new(Session::builder().with_stderr_emitter().build());
72 let source_status = compiler.enter_mut(|compiler| -> Result<_> {
73 let mut pcx = compiler.parse();
74 pcx.set_resolve_imports(true);
75 let status =
76 configure_pcx_all_sources_with_status(&mut pcx, &config, Some(&project), None)?;
77 pcx.parse();
78 Ok(status)
79 })?;
80
81 let mut output = if source_status.is_fully_supported() {
84 None
85 } else {
86 let mut compile_project = config.solar_project()?;
87 compile_project.no_artifacts = true;
88 Some(ProjectCompiler::new().compile(&compile_project)?)
89 };
90
91 let mut doc_cfg = config.doc;
92 if let Some(out) = self.out.clone() {
93 doc_cfg.out = out;
94 }
95
96 if doc_cfg.repository.is_none()
98 && let Some(remote) = Git::new(root).remote_url("origin")
99 && let Some(captures) = GH_REPO_PREFIX_REGEX.captures(&remote)
100 {
101 let brand = captures.name("brand").unwrap().as_str();
102 let tld = captures.name("tld").unwrap().as_str();
103 let project_path = GH_REPO_PREFIX_REGEX.replace(&remote, "");
104 doc_cfg.repository =
105 Some(format!("https://{brand}.{tld}/{}", project_path.trim_end_matches(".git")));
106 }
107
108 let git = Git::new(root);
109 let commit = doc_cfg.commit.clone().or_else(|| git.commit_hash(false, "HEAD").ok());
110 let branch = git.current_rev_branch(root).ok().map(|(_, b)| b).filter(|b| b != "HEAD");
113
114 let mut builder = DocBuilder::new(
115 root.clone(),
116 project.paths.sources,
117 project.paths.libraries,
118 self.include_libraries,
119 );
120 builder.commit = commit;
121 builder.branch = branch;
122 builder.deployments = self.deployments;
123 builder.config = doc_cfg.clone();
124
125 let out_dir = if doc_cfg.out.is_absolute() { doc_cfg.out } else { root.join(&doc_cfg.out) };
126
127 if !shell::is_quiet() {
128 sh_println!("Generating documentation...")?;
129 }
130 let started = Instant::now();
131 let compiler = if let Some(output) = &mut output {
132 output.parser_mut().solc_mut().compiler_mut()
133 } else {
134 &mut compiler
135 };
136 let stats = builder.build(compiler)?;
137 let elapsed = started.elapsed();
138
139 if !shell::is_quiet() {
140 sh_println!(
141 "Generated {pages} page{ps} from {sources} source{ss} in {elapsed:.2?} (render {render:.2?}, site {site:.2?})",
142 pages = stats.pages,
143 ps = if stats.pages <= 1 { "" } else { "s" },
144 sources = stats.sources,
145 ss = if stats.sources <= 1 { "" } else { "s" },
146 elapsed = elapsed,
147 render = stats.render_elapsed,
148 site = stats.site_elapsed,
149 )?;
150
151 sh_println!(
154 "\nDocumentation written to: {}\n\nTo preview:\n cd {}\n npm install --legacy-peer-deps\n npm run dev",
155 out_dir.display(),
156 out_dir.display(),
157 )?;
158 }
159
160 Ok(())
161 }
162
163 pub const fn is_watch(&self) -> bool {
165 self.watch.watch.is_some()
166 }
167
168 pub fn config(&self) -> Result<Config> {
169 load_config_with_root(self.root.as_deref())
170 }
171}