mirror of
https://github.com/denoland/deno.git
synced 2024-11-25 15:29:32 -05:00
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
This commit is contained in:
parent
f8f4e77632
commit
8ea2d926a9
4 changed files with 302 additions and 80 deletions
|
@ -105,11 +105,18 @@ 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
|
||||
|
@ -1360,6 +1373,30 @@ Show documentation for runtime built-ins:
|
|||
.help("Output documentation in JSON format")
|
||||
.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")
|
||||
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
154
cli/tools/doc.rs
154
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,33 +12,30 @@ 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> {
|
||||
let factory = CliFactory::from_flags(flags).await?;
|
||||
let cli_options = factory.cli_options();
|
||||
let module_info_cache = factory.module_info_cache()?;
|
||||
let source_parser = deno_graph::DefaultModuleParser::new_for_analysis();
|
||||
let store = DefaultParsedSourceStore::default();
|
||||
let analyzer =
|
||||
module_info_cache.as_module_analyzer(Some(&source_parser), &store);
|
||||
let capturing_parser =
|
||||
CapturingModuleParser::new(Some(&source_parser), &store);
|
||||
|
||||
let mut doc_nodes = match doc_flags.source_files {
|
||||
DocSourceFileFlag::Builtin => {
|
||||
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());
|
||||
|
@ -59,7 +56,7 @@ pub async fn print_docs(
|
|||
vec![source_file_specifier.clone()],
|
||||
&mut loader,
|
||||
deno_graph::BuildOptions {
|
||||
module_analyzer: Some(&analyzer),
|
||||
module_analyzer: Some(analyzer),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
|
@ -68,21 +65,50 @@ pub async fn print_docs(
|
|||
&graph,
|
||||
capturing_parser,
|
||||
doc::DocParserOptions {
|
||||
private: doc_flags.private,
|
||||
diagnostics: false,
|
||||
private: doc_flags.private,
|
||||
},
|
||||
)?;
|
||||
doc_parser.parse_module(&source_file_specifier)?.definitions
|
||||
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()?;
|
||||
let source_parser = deno_graph::DefaultModuleParser::new_for_analysis();
|
||||
let store = DefaultParsedSourceStore::default();
|
||||
let analyzer =
|
||||
module_info_cache.as_module_analyzer(Some(&source_parser), &store);
|
||||
let capturing_parser =
|
||||
CapturingModuleParser::new(Some(&source_parser), &store);
|
||||
|
||||
let doc_nodes_by_url = match doc_flags.source_files {
|
||||
DocSourceFileFlag::Builtin => {
|
||||
generate_doc_nodes_for_builtin_types(
|
||||
doc_flags.clone(),
|
||||
cli_options,
|
||||
capturing_parser,
|
||||
&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,17 +148,73 @@ 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 {
|
||||
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,
|
||||
};
|
||||
|
||||
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());
|
||||
let nodes = doc::find_nodes_by_name_recursively(doc_nodes, filter.clone());
|
||||
if nodes.is_empty() {
|
||||
bail!("Node {} was not found!", filter);
|
||||
}
|
||||
|
@ -142,16 +225,11 @@ pub async fn print_docs(
|
|||
} else {
|
||||
format!(
|
||||
"{}",
|
||||
doc::DocPrinter::new(
|
||||
&doc_nodes,
|
||||
colors::use_color(),
|
||||
doc_flags.private
|
||||
)
|
||||
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> {
|
||||
|
|
Loading…
Reference in a new issue