/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/tools/doc.rs
755 строк
21 KB
Nathan Whitaker
fix(resolver): scope sloppy import hints to prepared modules (#36221)
23 июл 2026, 04:23
Не верифицирован
23 июл 2026, 04:23
b23cb76
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::collections::BTreeMap; use std::path::Path; use std::sync::Arc; use deno_ast::MediaType; use deno_ast::diagnostics::Diagnostic; use deno_ast::diagnostics::DiagnosticLevel; use deno_ast::diagnostics::DiagnosticLocation; use deno_ast::diagnostics::DiagnosticSnippet; use deno_ast::diagnostics::DiagnosticSnippetHighlight; use deno_ast::diagnostics::DiagnosticSnippetHighlightStyle; use deno_ast::diagnostics::DiagnosticSourcePos; use deno_ast::diagnostics::DiagnosticSourceRange; use deno_config::glob::FilePatterns; use deno_config::glob::PathOrPatternSet; use deno_core::anyhow::Context; use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_core::serde_json; use deno_doc as doc; use deno_doc::Document; use deno_doc::ParseOutput; use deno_doc::html::UrlResolveKind; use deno_doc::html::UsageComposer; use deno_doc::html::UsageComposerEntry; use deno_doc::js_doc::JsDoc; use deno_graph::CheckJsOption; use deno_graph::GraphKind; use deno_graph::ModuleSpecifier; use deno_graph::analysis::ModuleAnalyzer; use deno_graph::ast::EsParser; use deno_graph::source::NullFileSystem; use deno_lib::version::DENO_VERSION_INFO; use deno_npm_installer::graph::NpmCachingStrategy; use doc::DocDiagnostic; use doc::html::ShortPath; use indexmap::IndexMap; use indexmap::IndexSet; use crate::args::DocFlags; use crate::args::DocHtmlFlag; use crate::args::DocSourceFileFlag; use crate::args::Flags; use crate::colors; use crate::display; use crate::factory::CliFactory; use crate::file_fetcher::TextDecodedFile; use crate::graph_util::GraphWalkErrorsOptions; use crate::graph_util::graph_exit_integrity_errors; use crate::graph_util::graph_walk_errors; use crate::sys::CliSys; use crate::tsc::get_types_declaration_file_text; use crate::util::fs::CollectSpecifiersOptions; use crate::util::fs::collect_specifiers; const JSON_SCHEMA_VERSION: u8 = 2; const PRISM_CSS: &str = include_str!("./doc/prism.css"); const PRISM_JS: &str = include_str!("./doc/prism.js"); fn char_highlight_end(text: &str, start_byte_index: usize) -> usize { if start_byte_index >= text.len() { return start_byte_index; } if let Some(text) = text.get(start_byte_index..) { return start_byte_index + text.chars().next().map(|c| c.len_utf8()).unwrap_or(1); } let mut end_byte_index = start_byte_index + 1; while end_byte_index < text.len() && !text.is_char_boundary(end_byte_index) { end_byte_index += 1; } end_byte_index } struct DisplayDocDiagnostic<'a>(&'a DocDiagnostic); impl Diagnostic for DisplayDocDiagnostic<'_> { fn level(&self) -> DiagnosticLevel { self.0.level() } fn code(&self) -> Cow<'_, str> { self.0.code() } fn message(&self) -> Cow<'_, str> { self.0.message() } fn location(&self) -> DiagnosticLocation<'_> { self.0.location() } fn snippet(&self) -> Option<DiagnosticSnippet<'_>> { let start_byte_index = self.0.location.byte_index; Some(DiagnosticSnippet { source: Cow::Borrowed(&self.0.text_info), highlights: vec![DiagnosticSnippetHighlight { style: DiagnosticSnippetHighlightStyle::Error, range: DiagnosticSourceRange { start: DiagnosticSourcePos::ByteIndex(start_byte_index), end: DiagnosticSourcePos::ByteIndex(char_highlight_end( self.0.text_info.text_str(), start_byte_index, )), }, description: None, }], }) } fn hint(&self) -> Option<Cow<'_, str>> { self.0.hint() } fn snippet_fixed(&self) -> Option<DiagnosticSnippet<'_>> { match &self.0.kind { doc::DocDiagnosticKind::PrivateTypeRef(diagnostic) => { let start_byte_index = diagnostic.reference_location.byte_index; Some(DiagnosticSnippet { source: Cow::Borrowed(&diagnostic.reference_text_info), highlights: vec![DiagnosticSnippetHighlight { style: DiagnosticSnippetHighlightStyle::Hint, range: DiagnosticSourceRange { start: DiagnosticSourcePos::ByteIndex(start_byte_index), end: DiagnosticSourcePos::ByteIndex(char_highlight_end( diagnostic.reference_text_info.text_str(), start_byte_index, )), }, description: Some(Cow::Borrowed("this is the referenced type")), }], }) } _ => None, } } fn info(&self) -> Cow<'_, [Cow<'_, str>]> { self.0.info() } fn docs_url(&self) -> Option<Cow<'_, str>> { self.0.docs_url() } } async fn generate_doc_nodes_for_builtin_types( doc_flags: DocFlags, parser: &dyn EsParser, analyzer: &dyn ModuleAnalyzer, ) -> Result<ParseOutput, AnyError> { let source_file_specifier = ModuleSpecifier::parse("file:///lib.deno.d.ts").unwrap(); let content = get_types_declaration_file_text(); let 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 roots = vec![source_file_specifier.clone()]; let mut graph = deno_graph::ModuleGraph::new(GraphKind::TypesOnly); graph .build( roots.clone(), Vec::new(), &loader, deno_graph::BuildOptions { is_dynamic: false, skip_dynamic_deps: false, passthrough_jsr_specifiers: false, executor: Default::default(), file_system: &NullFileSystem, jsr_metadata_store: None, jsr_url_provider: Default::default(), jsr_version_resolver: Default::default(), locker: None, module_analyzer: analyzer, module_info_cacher: Default::default(), npm_resolver: None, reporter: None, resolver: None, unstable_bytes_imports: false, unstable_text_imports: true, unstable_css_imports: false, unstable_config_imports: false, }, ) .await; let doc_parser = doc::DocParser::new( &graph, parser, &roots, doc::DocParserOptions { diagnostics: false, private: doc_flags.private, }, )?; Ok(doc_parser.parse()?) } pub async fn doc( flags: Arc<Flags>, doc_flags: DocFlags, ) -> Result<(), AnyError> { let factory = CliFactory::from_flags(flags); let cli_options = factory.cli_options()?; let module_info_cache = factory.module_info_cache()?; let parsed_source_cache = factory.parsed_source_cache()?; let capturing_parser = parsed_source_cache.as_capturing_parser(); let analyzer = module_info_cache.as_module_analyzer(); let file_fetcher = factory.file_fetcher()?; let documents_by_url = match doc_flags.source_files { DocSourceFileFlag::Builtin => { generate_doc_nodes_for_builtin_types( doc_flags.clone(), &capturing_parser, &analyzer, ) .await? } DocSourceFileFlag::Paths(ref source_files) => { let module_graph_creator = factory.module_graph_creator().await?; let sys = CliSys::default(); let module_specifiers = collect_specifiers( CollectSpecifiersOptions { file_patterns: FilePatterns { base: cli_options.initial_cwd().to_path_buf(), include: Some( PathOrPatternSet::from_include_relative_path_or_patterns( cli_options.initial_cwd(), source_files, )?, ), exclude: Default::default(), }, vendor_folder: cli_options.vendor_dir_path().map(ToOwned::to_owned), include_ignored_specified: false, }, |_| true, )?; let graph = module_graph_creator .create_graph( GraphKind::TypesOnly, module_specifiers.clone(), NpmCachingStrategy::Eager, ) .await?; // building the graph may have changed npm specifiers to // file: specifiers, so get the new roots let mut module_specifiers = graph.roots.iter().cloned().collect::<Vec<_>>(); graph_exit_integrity_errors(&graph); let collect_bare_importable_pkg_names = Vec::<String>::new; let errors = graph_walk_errors( &graph, &sys, &module_specifiers, GraphWalkErrorsOptions { check_js: CheckJsOption::False, kind: GraphKind::TypesOnly, will_type_check: false, allow_unknown_media_types: false, allow_unknown_jsr_exports: false, allow_sloppy_imports_hints_for_unreferenced_roots: true, collect_bare_importable_pkg_names: &collect_bare_importable_pkg_names, }, ); let mut markdown_urls = IndexSet::new(); for error in errors { if let Some(deno_graph::ModuleErrorKind::UnsupportedMediaType { media_type: MediaType::Markdown, specifier, .. }) = error.original().as_module_error_kind() { markdown_urls.insert(specifier.clone()); } else { log::warn!("{} {}", colors::yellow("Warning"), error); } } module_specifiers.retain(|s| { !markdown_urls.contains(s) && !markdown_urls.contains(graph.resolve(s)) }); let doc_parser = doc::DocParser::new( &graph, &capturing_parser, &module_specifiers, doc::DocParserOptions { private: doc_flags.private, diagnostics: doc_flags.lint, }, )?; let mut doc_nodes_by_url = doc_parser.parse()?; if doc_flags.lint { let mut diagnostics = doc_parser.take_diagnostics(); // Drop private-type-ref diagnostics whose referenced type lives // in a different package (i.e. a non-file specifier such as // jsr:, npm:, http:, https:). Those types' visibility is the // upstream package's concern, not the current package's. diagnostics.retain(|diagnostic| match &diagnostic.kind { doc::DocDiagnosticKind::PrivateTypeRef(d) => { d.reference_location.filename.starts_with("file:") } _ => true, }); check_diagnostics(&diagnostics)?; } let markdown_downloads = deno_core::futures::future::try_join_all( markdown_urls.into_iter().map(|url| async { // ok to skip permissions because these were provided on the CLI let file = file_fetcher.fetch_bypass_permissions(&url).await?; let decoded = TextDecodedFile::decode(file)?; Ok::<_, AnyError>((url, decoded)) }), ) .await?; for (url, decoded) in markdown_downloads { doc_nodes_by_url.insert( url, Document { module_doc: JsDoc { doc: Some(Box::from(&*decoded.source)), tags: Default::default(), }, imports: Default::default(), symbols: Default::default(), }, ); } doc_nodes_by_url } }; if let Some(html_options) = &doc_flags.html { let deno_ns = if doc_flags.source_files != DocSourceFileFlag::Builtin { let deno_ns = generate_doc_nodes_for_builtin_types( doc_flags.clone(), &capturing_parser, &analyzer, ) .await?; let (_, deno_ns) = deno_ns.into_iter().next().unwrap(); Some(deno_ns) } else { None }; let mut main_entrypoint = None; let rewrite_map = if let Some(config_file) = cli_options.start_dir.member_or_root_deno_json() { let config = config_file.to_exports_config()?; main_entrypoint = config.get_resolved(".").ok().flatten(); let rewrite_map = config .clone() .into_map() .into_keys() .map(|key| { Ok(( config.get_resolved(&key)?.unwrap(), key .strip_prefix('.') .unwrap_or(&key) .strip_prefix('/') .unwrap_or(&key) .to_owned(), )) }) .collect::<Result<IndexMap<_, _>, AnyError>>()?; Some(rewrite_map) } else { None }; generate_docs_directory( cli_options.initial_cwd(), documents_by_url, html_options, deno_ns, rewrite_map, main_entrypoint, ) } else { let modules_len = documents_by_url.len(); if doc_flags.json { let json_output = serde_json::json!({ "version": JSON_SCHEMA_VERSION, "nodes": &documents_by_url }); display::write_json_to_stdout(&json_output) } else if doc_flags.lint { // don't output docs if running with only the --lint flag log::info!( "Checked {} file{}", modules_len, if modules_len == 1 { "" } else { "s" } ); Ok(()) } else { print_docs_to_stdout(doc_flags, documents_by_url) } } } struct DocResolver { deno_ns: std::collections::HashMap<Vec<String>, Option<Arc<ShortPath>>>, strip_trailing_html: bool, } impl deno_doc::html::HrefResolver for DocResolver { fn resolve_path( &self, current: UrlResolveKind, target: UrlResolveKind, ) -> String { let path = deno_doc::html::href_path_resolve(current, target); if self.strip_trailing_html && let Some(path) = path .strip_suffix("index.html") .or_else(|| path.strip_suffix(".html")) { return path.to_owned(); } path } fn resolve_global_symbol(&self, symbol: &[String]) -> Option<String> { if self.deno_ns.contains_key(symbol) { Some(format!( "https://deno.land/api@v{}?s={}", DENO_VERSION_INFO.deno, symbol.join(".") )) } else { None } } fn resolve_import_href( &self, symbol: &[String], src: &str, ) -> Option<String> { let mut url = ModuleSpecifier::parse(src).ok()?; if url.domain() == Some("deno.land") { url.set_query(Some(&format!("s={}", symbol.join(".")))); return Some(url.into()); } None } fn resolve_source(&self, location: &deno_doc::Location) -> Option<String> { Some(location.filename.to_string()) } fn resolve_external_jsdoc_module( &self, module: &str, _symbol: Option<&str>, ) -> Option<(String, String)> { if let Ok(url) = deno_core::url::Url::parse(module) { match url.scheme() { "npm" => { let res = deno_semver::npm::NpmPackageReqReference::from_str(module).ok()?; let name = &res.req().name; Some(( format!("https://www.npmjs.com/package/{name}"), name.to_string(), )) } "jsr" => { let res = deno_semver::jsr::JsrPackageReqReference::from_str(module).ok()?; let name = &res.req().name; Some((format!("https://jsr.io/{name}"), name.to_string())) } _ => None, } } else { None } } } struct DocComposer; impl UsageComposer for DocComposer { fn is_single_mode(&self) -> bool { true } fn compose( &self, current_resolve: UrlResolveKind, usage_to_md: deno_doc::html::UsageToMd, ) -> IndexMap<UsageComposerEntry, String> { current_resolve .get_file() .map(|current_file| { IndexMap::from([( UsageComposerEntry { name: "".to_string(), icon: None, }, usage_to_md(current_file.path.as_str(), None), )]) }) .unwrap_or_default() } } fn generate_docs_directory( cwd: &Path, documents_by_url: ParseOutput, html_options: &DocHtmlFlag, built_in_types: Option<Document>, rewrite_map: Option<IndexMap<ModuleSpecifier, String>>, main_entrypoint: Option<ModuleSpecifier>, ) -> Result<(), AnyError> { let output_dir_resolved = cwd.join(&html_options.output); let category_docs = if let Some(category_docs_path) = &html_options.category_docs_path { let content = std::fs::read(category_docs_path)?; Some(serde_json::from_slice(&content)?) } else { None }; let symbol_redirect_map = if let Some(symbol_redirect_map_path) = &html_options.symbol_redirect_map_path { let content = std::fs::read(symbol_redirect_map_path)?; Some(serde_json::from_slice(&content)?) } else { None }; let default_symbol_map = if let Some(default_symbol_map_path) = &html_options.default_symbol_map_path { let content = std::fs::read(default_symbol_map_path)?; Some(serde_json::from_slice(&content)?) } else { None }; let mut options = deno_doc::html::GenerateOptions { package_name: html_options.name.clone(), main_entrypoint, rewrite_map, href_resolver: Arc::new(DocResolver { deno_ns: Default::default(), strip_trailing_html: html_options.strip_trailing_html, }), usage_composer: Some(Arc::new(DocComposer)), category_docs, disable_search: false, symbol_redirect_map, default_symbol_map, diff_only: false, markdown_renderer: deno_doc::html::comrak::create_renderer( None, None, None, ), markdown_stripper: Arc::new(deno_doc::html::comrak::strip), head_inject: Some(Arc::new(|root| { format!( r#"<link href="{root}{}" rel="stylesheet" /><link href="{root}prism.css" rel="stylesheet" /><script src="{root}prism.js"></script>"#, deno_doc::html::comrak::COMRAK_STYLESHEET_FILENAME ) })), id_prefix: None, }; if let Some(built_in_types) = built_in_types { let ctx = deno_doc::html::GenerateCtx::create_basic( deno_doc::html::GenerateOptions { package_name: None, main_entrypoint: Some( ModuleSpecifier::parse("file:///lib.deno.d.ts").unwrap(), ), href_resolver: Arc::new(DocResolver { deno_ns: Default::default(), strip_trailing_html: false, }), usage_composer: Some(Arc::new(DocComposer)), rewrite_map: Default::default(), category_docs: Default::default(), disable_search: Default::default(), symbol_redirect_map: Default::default(), default_symbol_map: Default::default(), diff_only: false, markdown_renderer: deno_doc::html::comrak::create_renderer( None, None, None, ), markdown_stripper: Arc::new(deno_doc::html::comrak::strip), head_inject: None, id_prefix: None, }, IndexMap::from([( ModuleSpecifier::parse("file:///lib.deno.d.ts").unwrap(), built_in_types, )]), None, )?; let deno_ns = deno_doc::html::compute_namespaced_symbols( &ctx, Box::new( ctx .doc_nodes .values() .next() .unwrap() .iter() .map(std::borrow::Cow::Borrowed), ), ); options.href_resolver = Arc::new(DocResolver { deno_ns, strip_trailing_html: html_options.strip_trailing_html, }); } let ctx = deno_doc::html::GenerateCtx::create_basic(options, documents_by_url, None)?; let mut files = deno_doc::html::generate(ctx) .context("Failed to generate HTML documentation")?; files.insert("prism.js".to_string(), PRISM_JS.to_string()); files.insert("prism.css".to_string(), PRISM_CSS.to_string()); 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_to_stdout( doc_flags: DocFlags, mut documents_by_url: ParseOutput, ) -> Result<(), AnyError> { if let Some(filter) = doc_flags.filter { for (_, doc) in &mut documents_by_url { let symbols = std::mem::take(&mut doc.symbols); doc.symbols = doc::find_nodes_by_name_recursively(symbols, &filter) .into_iter() .map(Arc::new) .collect(); if doc.symbols.is_empty() { bail!("Node {} was not found!", filter); } } } let details = format!( "{}", doc::DocPrinter::new( &documents_by_url, colors::use_color(), doc_flags.private ) ); deno_print::drop_write_stdout(details.as_bytes()); Ok(()) } fn check_diagnostics(diagnostics: &[DocDiagnostic]) -> Result<(), AnyError> { if diagnostics.is_empty() { return Ok(()); } // group by location then by line (sorted) then column (sorted) let mut diagnostic_groups = IndexMap::new(); for diagnostic in diagnostics { diagnostic_groups .entry(diagnostic.location.filename.clone()) .or_insert_with(BTreeMap::new) .entry(diagnostic.location.line) .or_insert_with(BTreeMap::new) .entry(diagnostic.location.col) .or_insert_with(Vec::new) .push(diagnostic); } for (_, diagnostics_by_lc) in diagnostic_groups { for (_, diagnostics_by_col) in diagnostics_by_lc { for (_, diagnostics) in diagnostics_by_col { for diagnostic in diagnostics { log::error!("{}\n", DisplayDocDiagnostic(diagnostic).display()); } } } } bail!( "Found {} documentation lint error{}.", colors::bold(diagnostics.len().to_string()), if diagnostics.len() == 1 { "" } else { "s" } ); }