1pub mod doc;
2pub mod render;
3pub mod search;
4
5use anyhow::{bail, Result};
6use clap::Parser;
7use doc::{module::ModuleInfo, Documentation};
8use forc_pkg::{
9 self as pkg,
10 manifest::{GenericManifestFile, ManifestFile},
11 source::IPFSNode,
12 PackageManifestFile, Programs,
13};
14use forc_tracing::println_action_green;
15use forc_util::default_output_directory;
16use render::{
17 index::{LibraryInfo, WorkspaceIndex},
18 HTMLString, Renderable, RenderedDocumentation,
19};
20use std::{
21 fs,
22 path::{Path, PathBuf},
23};
24use sway_core::{
25 language::ty::{TyProgram, TyProgramKind},
26 BuildTarget, Engines,
27};
28use sway_features::ExperimentalFeatures;
29
30pub const DOC_DIR_NAME: &str = "doc";
31pub const ASSETS_DIR_NAME: &str = "static.files";
32
33forc_util::cli_examples! {
34 crate::Command {
35 [ Build the docs for a project or workspace in the current path => "forc doc"]
36 [ Build the docs for a project or workspace in the current path and open it in the browser => "forc doc --open" ]
37 [ Build the docs for a project located in another path => "forc doc --path {path}" ]
38 [ Build the docs for the current project exporting private types => "forc doc --document-private-items" ]
39 [ Build the docs offline without downloading any dependencies => "forc doc --offline" ]
40 }
41}
42
43#[derive(Debug, Parser, Default)]
45#[clap(
46 name = "forc-doc",
47 after_help = help(),
48 version
49)]
50pub struct Command {
51 #[clap(short, long, alias = "manifest-path")]
55 pub path: Option<String>,
56 #[clap(long)]
58 pub document_private_items: bool,
59 #[clap(long)]
61 pub open: bool,
62 #[clap(long)]
65 pub offline: bool,
66 #[clap(long)]
69 pub locked: bool,
70 #[clap(long)]
72 pub no_deps: bool,
73 #[clap(long)]
77 pub ipfs_node: Option<IPFSNode>,
78 #[clap(long)]
82 pub doc_path: Option<String>,
83 #[clap(flatten)]
84 pub experimental: sway_features::CliFields,
85 #[clap(long, short = 's', action)]
87 pub silent: bool,
88}
89
90#[derive(Debug, Clone)]
92pub enum DocResult {
93 Package(Box<PackageManifestFile>),
94 Workspace {
95 name: String,
96 libraries: Vec<LibraryInfo>,
97 },
98}
99
100pub fn generate_docs(opts: &Command) -> Result<(PathBuf, DocResult)> {
102 let ctx = DocContext::from_options(opts)?;
103 let mut compile_results = compile(&ctx, opts)?.collect::<Vec<_>>();
104 let doc_result = compile_html(opts, &ctx, &mut compile_results)?;
105 Ok((ctx.doc_path, doc_result))
106}
107
108#[derive(Clone)]
110pub struct RenderPlan<'e> {
111 no_deps: bool,
112 document_private_items: bool,
113 engines: &'e Engines,
114}
115
116impl<'e> RenderPlan<'e> {
117 pub fn new(
118 no_deps: bool,
119 document_private_items: bool,
120 engines: &'e Engines,
121 ) -> RenderPlan<'e> {
122 Self {
123 no_deps,
124 document_private_items,
125 engines,
126 }
127 }
128}
129
130pub struct DocContext {
131 pub manifest: ManifestFile,
132 pub doc_path: PathBuf,
133 pub engines: Engines,
134 pub build_plan: pkg::BuildPlan,
135 pub workspace_name: String,
136}
137
138impl DocContext {
139 pub fn is_workspace(&self) -> bool {
140 matches!(self.manifest, ManifestFile::Workspace(_))
141 }
142
143 pub fn pkg_manifest(&self) -> Option<&PackageManifestFile> {
145 match &self.manifest {
146 ManifestFile::Package(pkg) => Some(pkg),
147 ManifestFile::Workspace(_) => None,
148 }
149 }
150
151 pub fn from_options(opts: &Command) -> Result<Self> {
152 let dir = if let Some(ref path) = opts.path {
154 PathBuf::from(path)
155 } else {
156 std::env::current_dir()?
157 };
158 let manifest = ManifestFile::from_dir(dir)?;
159
160 let workspace_name = manifest
162 .dir()
163 .file_name()
164 .and_then(|name| name.to_str())
165 .unwrap_or("workspace")
166 .to_string();
167
168 let out_path = default_output_directory(manifest.dir());
170 let doc_dir = opts
171 .doc_path
172 .clone()
173 .unwrap_or_else(|| DOC_DIR_NAME.to_string());
174 let doc_path = out_path.join(doc_dir);
175 if doc_path.exists() {
176 std::fs::remove_dir_all(&doc_path)?;
177 }
178 fs::create_dir_all(&doc_path)?;
179
180 let member_manifests = manifest.member_manifests()?;
182 let lock_path = manifest.lock_path()?;
183
184 if matches!(manifest, ManifestFile::Workspace(_)) && member_manifests.is_empty() {
186 bail!("Workspace contains no members");
187 }
188
189 let ipfs_node = opts.ipfs_node.clone().unwrap_or_default();
190 let build_plan = pkg::BuildPlan::from_lock_and_manifests(
191 &lock_path,
192 &member_manifests,
193 opts.locked,
194 opts.offline,
195 &ipfs_node,
196 )?;
197
198 Ok(Self {
199 manifest,
200 doc_path,
201 engines: Engines::default(),
202 build_plan,
203 workspace_name,
204 })
205 }
206}
207
208pub fn compile(ctx: &DocContext, opts: &Command) -> Result<impl Iterator<Item = Option<Programs>>> {
209 if ctx.is_workspace() {
210 println_action_green(
211 "Compiling",
212 &format!("workspace ({})", ctx.manifest.dir().to_string_lossy()),
213 );
214 } else if let Some(pkg_manifest) = ctx.pkg_manifest() {
215 println_action_green(
216 "Compiling",
217 &format!(
218 "{} ({})",
219 pkg_manifest.project_name(),
220 ctx.manifest.dir().to_string_lossy()
221 ),
222 );
223 }
224
225 let tests_enabled = opts.document_private_items;
226 pkg::check(
227 &ctx.build_plan,
228 BuildTarget::default(),
229 opts.silent,
230 None,
231 tests_enabled,
232 &ctx.engines,
233 None,
234 &opts.experimental.experimental,
235 &opts.experimental.no_experimental,
236 sway_core::DbgGeneration::Full,
237 )
238 .map(|results| results.into_iter().map(|(programs, _handler)| programs))
239}
240
241pub fn compile_html(
242 opts: &Command,
243 ctx: &DocContext,
244 compile_results: &mut Vec<Option<Programs>>,
245) -> Result<DocResult> {
246 let mut documented_libraries = Vec::new();
247
248 let raw_docs = if opts.no_deps {
249 if let Some(pkg_manifest) = ctx.pkg_manifest() {
250 let Some(ty_program) = compile_results
252 .pop()
253 .and_then(|programs| programs)
254 .and_then(|p| p.typed.ok())
255 else {
256 bail! {
257 "documentation could not be built from manifest located at '{}'",
258 pkg_manifest.path().display()
259 }
260 };
261
262 if matches!(ty_program.kind, TyProgramKind::Library { .. }) {
264 let lib_info = LibraryInfo {
265 name: pkg_manifest.project_name().to_string(),
266 description: pkg_manifest
267 .project
268 .description
269 .clone()
270 .unwrap_or_else(|| format!("Library {}", pkg_manifest.project_name())),
271 };
272 documented_libraries.push(lib_info);
273 build_docs(opts, ctx, &ty_program, &ctx.manifest, pkg_manifest)?
274 } else {
275 bail!(
276 "forc-doc only supports libraries. '{}' is not a library.",
277 pkg_manifest.project_name()
278 );
279 }
280 } else {
281 bail!("--no-deps flag is not meaningful for workspaces");
283 }
284 } else {
285 let (order, graph, manifest_map) = (
286 ctx.build_plan.compilation_order(),
287 ctx.build_plan.graph(),
288 ctx.build_plan.manifest_map(),
289 );
290 let mut raw_docs = Documentation(Vec::new());
291
292 for (node, compile_result) in order.iter().zip(compile_results) {
293 let id = &graph[*node].id();
294 if let Some(pkg_manifest_file) = manifest_map.get(id) {
295 let manifest_file = ManifestFile::from_dir(pkg_manifest_file.path())?;
296 let ty_program = compile_result
297 .as_ref()
298 .and_then(|programs| programs.typed.clone().ok())
299 .ok_or_else(|| {
300 anyhow::anyhow!(
301 "documentation could not be built from manifest located at '{}'",
302 pkg_manifest_file.path().display()
303 )
304 })?;
305
306 if matches!(ty_program.kind, TyProgramKind::Library { .. }) {
308 let is_workspace_member = if ctx.is_workspace() {
310 ctx.manifest.member_manifests()?.iter().any(|(_, member)| {
311 member.project_name() == pkg_manifest_file.project_name()
312 })
313 } else {
314 true };
316
317 if is_workspace_member {
318 let lib_info = LibraryInfo {
319 name: pkg_manifest_file.project_name().to_string(),
320 description: pkg_manifest_file
321 .project
322 .description
323 .clone()
324 .unwrap_or_else(|| {
325 format!("Library {}", pkg_manifest_file.project_name())
326 }),
327 };
328 documented_libraries.push(lib_info);
329 raw_docs.0.extend(
330 build_docs(opts, ctx, &ty_program, &manifest_file, pkg_manifest_file)?
331 .0,
332 );
333 }
334 }
335 }
336 }
337 raw_docs
338 };
339
340 if ctx.is_workspace() && !documented_libraries.is_empty() {
342 documented_libraries.sort_by(|a, b| a.name.cmp(&b.name));
344 create_workspace_index(
345 &ctx.doc_path,
346 &documented_libraries,
347 &ctx.engines,
348 &ctx.workspace_name,
349 )?;
350 }
351
352 search::write_search_index(&ctx.doc_path, &raw_docs)?;
353
354 let result = if ctx.is_workspace() {
355 DocResult::Workspace {
356 name: ctx.workspace_name.clone(),
357 libraries: documented_libraries,
358 }
359 } else if let Some(pkg_manifest) = ctx.pkg_manifest() {
360 DocResult::Package(Box::new(pkg_manifest.clone()))
361 } else {
362 unreachable!("Should have either workspace or package")
363 };
364
365 Ok(result)
366}
367
368fn build_docs(
369 opts: &Command,
370 ctx: &DocContext,
371 ty_program: &TyProgram,
372 manifest: &ManifestFile,
373 pkg_manifest: &PackageManifestFile,
374) -> Result<Documentation> {
375 let experimental = ExperimentalFeatures::new(
376 &pkg_manifest.project.experimental,
377 &opts.experimental.experimental,
378 &opts.experimental.no_experimental,
379 )
380 .map_err(|err| anyhow::anyhow!("{err}"))?;
381
382 println_action_green(
383 "Building",
384 &format!(
385 "documentation for {} ({})",
386 pkg_manifest.project_name(),
387 manifest.dir().to_string_lossy()
388 ),
389 );
390
391 let raw_docs = Documentation::from_ty_program(
392 &ctx.engines,
393 pkg_manifest.project_name(),
394 ty_program,
395 opts.document_private_items,
396 experimental,
397 )?;
398 let root_attributes = (!ty_program.root_module.attributes.is_empty())
399 .then_some(ty_program.root_module.attributes.clone());
400 let forc_version = pkg_manifest
401 .project
402 .forc_version
403 .as_ref()
404 .map(|ver| format!("Forc v{}.{}.{}", ver.major, ver.minor, ver.patch));
405 let rendered_docs = RenderedDocumentation::from_raw_docs(
407 raw_docs.clone(),
408 RenderPlan::new(opts.no_deps, opts.document_private_items, &ctx.engines),
409 root_attributes,
410 &ty_program.kind,
411 forc_version,
412 )?;
413
414 write_content(rendered_docs, &ctx.doc_path)?;
416 println_action_green("Finished", pkg_manifest.project_name());
417
418 Ok(raw_docs)
419}
420
421fn write_content(rendered_docs: RenderedDocumentation, doc_path: &Path) -> Result<()> {
422 for doc in rendered_docs.0 {
423 let mut doc_path = doc_path.to_path_buf();
424 for prefix in doc.module_info.module_prefixes {
425 doc_path.push(prefix);
426 }
427 fs::create_dir_all(&doc_path)?;
428 doc_path.push(doc.html_filename);
429 fs::write(&doc_path, doc.file_contents.0.as_bytes())?;
430 }
431 Ok(())
432}
433
434fn create_workspace_index(
435 doc_path: &Path,
436 documented_libraries: &[LibraryInfo],
437 engines: &Engines,
438 workspace_name: &str,
439) -> Result<()> {
440 let workspace_info = ModuleInfo::from_ty_module(vec![workspace_name.to_string()], None);
442
443 let workspace_index = WorkspaceIndex::new(workspace_info, documented_libraries.to_vec());
445
446 let render_plan = RenderPlan::new(false, false, engines);
447 let rendered_content = workspace_index.render(render_plan)?;
448 let html_content = HTMLString::from_rendered_content(rendered_content)?;
449
450 fs::write(doc_path.join("index.html"), html_content.0.as_bytes())?;
451 Ok(())
452}