diff options
author | Bartek IwaĆczuk <biwanczuk@gmail.com> | 2023-11-01 16:25:05 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-01 15:25:05 +0000 |
commit | 8ea2d926a9c75b0b13e7ad37e7181657d680560e (patch) | |
tree | 6a96beb4d9a877bf774dadb9f94cbf31134fe1fc | |
parent | f8f4e776325efe0d8dd50207beecb425f0875999 (diff) |
feat: deno doc --html (#21015)
This commit adds static documentation site generate to "deno doc"
subcommand.
Example:
```
$ deno doc --html --name="My library" ./mod.ts
# outputs to ./docs/
$ deno doc --html --name="My library" --output=./documentation/ ./mod.ts ./file2.js
# outputs to ./documentation/
$ deno doc --html --name="My library" ./**/mod.ts
# generate docs for all files with "mod.ts" name
```
Closes https://github.com/denoland/deno/issues/8233
-rw-r--r-- | cli/args/flags.rs | 123 | ||||
-rw-r--r-- | cli/main.rs | 2 | ||||
-rw-r--r-- | cli/tests/integration/doc_tests.rs | 35 | ||||
-rw-r--r-- | cli/tools/doc.rs | 220 |
4 files changed, 301 insertions, 79 deletions
diff --git a/cli/args/flags.rs b/cli/args/flags.rs index b3fe61abd..dbc868efa 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -106,10 +106,17 @@ impl Default for DocSourceFileFlag { } #[derive(Clone, Debug, Eq, PartialEq)] +pub struct DocHtmlFlag { + pub name: String, + pub output: PathBuf, +} + +#[derive(Clone, Debug, Eq, PartialEq)] pub struct DocFlags { pub private: bool, pub json: bool, pub lint: bool, + pub html: Option<DocHtmlFlag>, pub source_files: DocSourceFileFlag, pub filter: Option<String>, } @@ -1325,6 +1332,12 @@ Output documentation to standard output: deno doc ./path/to/module.ts +Output documentation in HTML format: + + deno doc --html --name=\"My library\" ./path/to/module.ts + deno doc --html --name=\"My library\" ./main.ts ./dev.ts + deno doc --html --name=\"My library\" --output=./documentation/ ./path/to/module.ts + Output private documentation to standard output: deno doc --private ./path/to/module.ts @@ -1361,6 +1374,30 @@ Show documentation for runtime built-ins: .action(ArgAction::SetTrue), ) .arg( + Arg::new("html") + .long("html") + .help("Output documentation in HTML format") + .action(ArgAction::SetTrue) + .conflicts_with("json") + ) + .arg( + Arg::new("name") + .long("name") + .help("The name that will be displayed in the docs") + .action(ArgAction::Set) + .required_if_eq("html", "true") + .require_equals(true) + ) + .arg( + Arg::new("output") + .long("output") + .help("Directory for HTML documentation output") + .action(ArgAction::Set) + .require_equals(true) + .value_hint(ValueHint::DirPath) + .value_parser(value_parser!(PathBuf)) + ) + .arg( Arg::new("private") .long("private") .help("Output private documentation") @@ -1372,7 +1409,8 @@ Show documentation for runtime built-ins: .help("Dot separated path to symbol") .required(false) .conflicts_with("json") - .conflicts_with("lint"), + .conflicts_with("lint") + .conflicts_with("html"), ) .arg( Arg::new("lint") @@ -3180,10 +3218,21 @@ fn doc_parse(flags: &mut Flags, matches: &mut ArgMatches) { let lint = matches.get_flag("lint"); let json = matches.get_flag("json"); let filter = matches.remove_one::<String>("filter"); + let html = if matches.get_flag("html") { + let name = matches.remove_one::<String>("name").unwrap(); + let output = matches + .remove_one::<PathBuf>("output") + .unwrap_or(PathBuf::from("./docs/")); + Some(DocHtmlFlag { name, output }) + } else { + None + }; + flags.subcommand = DenoSubcommand::Doc(DocFlags { source_files, json, lint, + html, filter, private, }); @@ -6085,6 +6134,7 @@ mod tests { source_files: DocSourceFileFlag::Paths(vec!["script.ts".to_owned()]), private: false, json: false, + html: None, lint: false, filter: None, }), @@ -7374,10 +7424,64 @@ mod tests { subcommand: DenoSubcommand::Doc(DocFlags { private: false, json: true, + html: None, lint: false, - source_files: DocSourceFileFlag::Paths(vec![ - "path/to/module.ts".to_string() - ]), + source_files: DocSourceFileFlag::Paths(svec!["path/to/module.ts"]), + filter: None, + }), + ..Flags::default() + } + ); + + let r = flags_from_vec(svec!["deno", "doc", "--html", "path/to/module.ts"]); + assert!(r.is_err()); + + let r = flags_from_vec(svec![ + "deno", + "doc", + "--html", + "--name=My library", + "path/to/module.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Doc(DocFlags { + private: false, + json: false, + lint: false, + html: Some(DocHtmlFlag { + name: "My library".to_string(), + output: PathBuf::from("./docs/"), + }), + source_files: DocSourceFileFlag::Paths(svec!["path/to/module.ts"]), + filter: None, + }), + ..Flags::default() + } + ); + + let r = flags_from_vec(svec![ + "deno", + "doc", + "--html", + "--name=My library", + "--lint", + "--output=./foo", + "path/to/module.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Doc(DocFlags { + private: false, + json: false, + html: Some(DocHtmlFlag { + name: "My library".to_string(), + output: PathBuf::from("./foo"), + }), + lint: true, + source_files: DocSourceFileFlag::Paths(svec!["path/to/module.ts"]), filter: None, }), ..Flags::default() @@ -7397,6 +7501,7 @@ mod tests { subcommand: DenoSubcommand::Doc(DocFlags { private: false, json: false, + html: None, lint: false, source_files: DocSourceFileFlag::Paths(vec![ "path/to/module.ts".to_string() @@ -7414,6 +7519,7 @@ mod tests { subcommand: DenoSubcommand::Doc(DocFlags { private: false, json: false, + html: None, lint: false, source_files: Default::default(), filter: None, @@ -7436,6 +7542,7 @@ mod tests { private: false, lint: false, json: false, + html: None, source_files: DocSourceFileFlag::Builtin, filter: Some("Deno.Listener".to_string()), }), @@ -7458,9 +7565,8 @@ mod tests { private: true, lint: false, json: false, - source_files: DocSourceFileFlag::Paths(vec![ - "path/to/module.js".to_string() - ]), + html: None, + source_files: DocSourceFileFlag::Paths(svec!["path/to/module.js"]), filter: None, }), no_npm: true, @@ -7482,6 +7588,7 @@ mod tests { private: false, lint: false, json: false, + html: None, source_files: DocSourceFileFlag::Paths(vec![ "path/to/module.js".to_string(), "path/to/module2.js".to_string() @@ -7505,6 +7612,7 @@ mod tests { subcommand: DenoSubcommand::Doc(DocFlags { private: false, json: false, + html: None, lint: false, source_files: DocSourceFileFlag::Paths(vec![ "path/to/module.js".to_string(), @@ -7530,6 +7638,7 @@ mod tests { private: false, lint: true, json: false, + html: None, source_files: DocSourceFileFlag::Paths(vec![ "path/to/module.js".to_string(), "path/to/module2.js".to_string() diff --git a/cli/main.rs b/cli/main.rs index dbd3b470b..7a8647a81 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -95,7 +95,7 @@ async fn run_subcommand(flags: Flags) -> Result<i32, AnyError> { tools::bundle::bundle(flags, bundle_flags).await }), DenoSubcommand::Doc(doc_flags) => { - spawn_subcommand(async { tools::doc::print_docs(flags, doc_flags).await }) + spawn_subcommand(async { tools::doc::doc(flags, doc_flags).await }) } DenoSubcommand::Eval(eval_flags) => spawn_subcommand(async { tools::run::eval_command(flags, eval_flags).await diff --git a/cli/tests/integration/doc_tests.rs b/cli/tests/integration/doc_tests.rs index a16f99dd9..f5ac44a02 100644 --- a/cli/tests/integration/doc_tests.rs +++ b/cli/tests/integration/doc_tests.rs @@ -64,6 +64,12 @@ itest!(deno_doc_lint_referenced_private_types_fixed { output: "doc/referenced_private_types_fixed.out", }); +itest!(deno_doc_html_lint_referenced_private_types_fixed { + args: "doc --lint --html --name=Library doc/referenced_private_types.ts", + exit_code: 1, + output: "doc/referenced_private_types_lint.out", +}); + itest!(_060_deno_doc_displays_all_overloads_in_details_view { args: "doc --filter NS.test doc/060_deno_doc_displays_all_overloads_in_details_view.ts", @@ -96,3 +102,32 @@ itest!(doc_no_lock { cwd: Some("lockfile/basic"), output: "lockfile/basic/doc.nolock.out", }); + +#[test] +fn deno_doc_html() { + let context = TestContext::default(); + let temp_dir = context.temp_dir(); + let output = context + .new_command() + .env("NO_COLOR", "1") + .args_vec(vec![ + "doc", + "--html", + "--name=MyLib", + &format!("--output={}", temp_dir.path().to_string_lossy()), + "doc/referenced_private_types_fixed.ts", + ]) + .split_output() + .run(); + + output.assert_exit_code(0); + assert_contains!(output.stderr(), "Written 8 files to"); + assert!(temp_dir.path().join("index.html").exists()); + assert!(temp_dir.path().join("compound_index.html").exists()); + assert!(temp_dir.path().join("fuse.js").exists()); + assert!(temp_dir.path().join("search.js").exists()); + assert!(temp_dir.path().join("search_index.js").exists()); + assert!(temp_dir.path().join("styles.css").exists()); + assert!(temp_dir.path().join("MyInterface.html").exists()); + assert!(temp_dir.path().join("MyClass.html").exists()); +} diff --git a/cli/tools/doc.rs b/cli/tools/doc.rs index 4028c412f..9c88c8e84 100644 --- a/cli/tools/doc.rs +++ b/cli/tools/doc.rs @@ -1,8 +1,8 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -use std::collections::BTreeMap; - +use crate::args::CliOptions; use crate::args::DocFlags; +use crate::args::DocHtmlFlag; use crate::args::DocSourceFileFlag; use crate::args::Flags; use crate::colors; @@ -12,21 +12,69 @@ use crate::factory::CliFactory; use crate::graph_util::graph_lock_or_exit; use crate::graph_util::CreateGraphOptions; use crate::tsc::get_types_declaration_file_text; +use crate::util::glob::expand_globs; use deno_core::anyhow::bail; +use deno_core::anyhow::Context; use deno_core::error::AnyError; +use deno_core::futures::FutureExt; use deno_core::resolve_url_or_path; use deno_doc as doc; use deno_graph::CapturingModuleParser; use deno_graph::DefaultParsedSourceStore; use deno_graph::GraphKind; +use deno_graph::ModuleAnalyzer; use deno_graph::ModuleSpecifier; use doc::DocDiagnostic; use indexmap::IndexMap; +use std::collections::BTreeMap; +use std::path::PathBuf; +use std::sync::Arc; -pub async fn print_docs( - flags: Flags, +async fn generate_doc_nodes_for_builtin_types( doc_flags: DocFlags, -) -> Result<(), AnyError> { + cli_options: &Arc<CliOptions>, + capturing_parser: CapturingModuleParser<'_>, + analyzer: &dyn ModuleAnalyzer, +) -> Result<IndexMap<ModuleSpecifier, Vec<doc::DocNode>>, AnyError> { + let source_file_specifier = + ModuleSpecifier::parse("internal://lib.deno.d.ts").unwrap(); + let content = get_types_declaration_file_text(cli_options.unstable()); + let mut loader = deno_graph::source::MemoryLoader::new( + vec![( + source_file_specifier.to_string(), + deno_graph::source::Source::Module { + specifier: source_file_specifier.to_string(), + content, + maybe_headers: None, + }, + )], + Vec::new(), + ); + let mut graph = deno_graph::ModuleGraph::new(GraphKind::TypesOnly); + graph + .build( + vec![source_file_specifier.clone()], + &mut loader, + deno_graph::BuildOptions { + module_analyzer: Some(analyzer), + ..Default::default() + }, + ) + .await; + let doc_parser = doc::DocParser::new( + &graph, + capturing_parser, + doc::DocParserOptions { + diagnostics: false, + private: doc_flags.private, + }, + )?; + let nodes = doc_parser.parse_module(&source_file_specifier)?.definitions; + + Ok(IndexMap::from([(source_file_specifier, nodes)])) +} + +pub async fn doc(flags: Flags, doc_flags: DocFlags) -> Result<(), AnyError> { let factory = CliFactory::from_flags(flags).await?; let cli_options = factory.cli_options(); let module_info_cache = factory.module_info_cache()?; @@ -37,52 +85,30 @@ pub async fn print_docs( let capturing_parser = CapturingModuleParser::new(Some(&source_parser), &store); - let mut doc_nodes = match doc_flags.source_files { + let doc_nodes_by_url = match doc_flags.source_files { DocSourceFileFlag::Builtin => { - let source_file_specifier = - ModuleSpecifier::parse("internal://lib.deno.d.ts").unwrap(); - let content = get_types_declaration_file_text(cli_options.unstable()); - let mut loader = deno_graph::source::MemoryLoader::new( - vec![( - source_file_specifier.to_string(), - deno_graph::source::Source::Module { - specifier: source_file_specifier.to_string(), - content, - maybe_headers: None, - }, - )], - Vec::new(), - ); - let mut graph = deno_graph::ModuleGraph::new(GraphKind::TypesOnly); - graph - .build( - vec![source_file_specifier.clone()], - &mut loader, - deno_graph::BuildOptions { - module_analyzer: Some(&analyzer), - ..Default::default() - }, - ) - .await; - let doc_parser = doc::DocParser::new( - &graph, + generate_doc_nodes_for_builtin_types( + doc_flags.clone(), + cli_options, capturing_parser, - doc::DocParserOptions { - private: doc_flags.private, - diagnostics: false, - }, - )?; - doc_parser.parse_module(&source_file_specifier)?.definitions + &analyzer, + ) + .await? } - DocSourceFileFlag::Paths(source_files) => { + DocSourceFileFlag::Paths(ref source_files) => { let module_graph_builder = factory.module_graph_builder().await?; let maybe_lockfile = factory.maybe_lockfile(); + let expanded_globs = + expand_globs(source_files.iter().map(PathBuf::from).collect())?; let module_specifiers: Result<Vec<ModuleSpecifier>, AnyError> = - source_files + expanded_globs .iter() .map(|source_file| { - Ok(resolve_url_or_path(source_file, cli_options.initial_cwd())?) + Ok(resolve_url_or_path( + &source_file.to_string_lossy(), + cli_options.initial_cwd(), + )?) }) .collect(); let module_specifiers = module_specifiers?; @@ -109,11 +135,12 @@ pub async fn print_docs( }, )?; - let mut doc_nodes = vec![]; + let mut doc_nodes_by_url = + IndexMap::with_capacity(module_specifiers.len()); - for module_specifier in module_specifiers { - let nodes = doc_parser.parse_with_reexports(&module_specifier)?; - doc_nodes.extend_from_slice(&nodes); + for module_specifier in &module_specifiers { + let nodes = doc_parser.parse_with_reexports(module_specifier)?; + doc_nodes_by_url.insert(module_specifier.clone(), nodes); } if doc_flags.lint { @@ -121,37 +148,88 @@ pub async fn print_docs( check_diagnostics(&diagnostics)?; } - doc_nodes + doc_nodes_by_url } }; - if doc_flags.json { - write_json_to_stdout(&doc_nodes) + if let Some(html_options) = doc_flags.html { + generate_docs_directory(&doc_nodes_by_url, html_options) + .boxed_local() + .await } else { - doc_nodes.retain(|doc_node| doc_node.kind != doc::DocNodeKind::Import); - let details = if let Some(filter) = doc_flags.filter { - let nodes = - doc::find_nodes_by_name_recursively(doc_nodes, filter.clone()); - if nodes.is_empty() { - bail!("Node {} was not found!", filter); - } - format!( - "{}", - doc::DocPrinter::new(&nodes, colors::use_color(), doc_flags.private) - ) - } else { - format!( - "{}", - doc::DocPrinter::new( - &doc_nodes, - colors::use_color(), - doc_flags.private - ) - ) - }; + let doc_nodes: Vec<doc::DocNode> = + doc_nodes_by_url.values().flatten().cloned().collect(); + print_docs(doc_flags, doc_nodes) + } +} + +async fn generate_docs_directory( + doc_nodes_by_url: &IndexMap<ModuleSpecifier, Vec<doc::DocNode>>, + html_options: DocHtmlFlag, +) -> Result<(), AnyError> { + let cwd = std::env::current_dir().context("Failed to get CWD")?; + let output_dir_resolved = cwd.join(&html_options.output); + + let options = deno_doc::html::GenerateOptions { + package_name: html_options.name, + }; - write_to_stdout_ignore_sigpipe(details.as_bytes()).map_err(AnyError::from) + let files = deno_doc::html::generate(options, doc_nodes_by_url) + .context("Failed to generate HTML documentation")?; + + let path = &output_dir_resolved; + let _ = std::fs::remove_dir_all(path); + std::fs::create_dir(path) + .with_context(|| format!("Failed to create directory {:?}", path))?; + + let no_of_files = files.len(); + for (name, content) in files { + let this_path = path.join(name); + let prefix = this_path.parent().with_context(|| { + format!("Failed to get parent path for {:?}", this_path) + })?; + std::fs::create_dir_all(prefix) + .with_context(|| format!("Failed to create directory {:?}", prefix))?; + std::fs::write(&this_path, content) + .with_context(|| format!("Failed to write file {:?}", this_path))?; } + + log::info!( + "{}", + colors::green(format!( + "Written {} files to {:?}", + no_of_files, html_options.output + )) + ); + Ok(()) +} + +fn print_docs( + doc_flags: DocFlags, + mut doc_nodes: Vec<deno_doc::DocNode>, +) -> Result<(), AnyError> { + if doc_flags.json { + return write_json_to_stdout(&doc_nodes); + } + + doc_nodes.retain(|doc_node| doc_node.kind != doc::DocNodeKind::Import); + let details = if let Some(filter) = doc_flags.filter { + let nodes = doc::find_nodes_by_name_recursively(doc_nodes, filter.clone()); + if nodes.is_empty() { + bail!("Node {} was not found!", filter); + } + format!( + "{}", + doc::DocPrinter::new(&nodes, colors::use_color(), doc_flags.private) + ) + } else { + format!( + "{}", + doc::DocPrinter::new(&doc_nodes, colors::use_color(), doc_flags.private) + ) + }; + + write_to_stdout_ignore_sigpipe(details.as_bytes()).map_err(AnyError::from) } fn check_diagnostics(diagnostics: &[DocDiagnostic]) -> Result<(), AnyError> { |