/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/tools/publish/module_content.rs
704 строки
21 KB
Nathan Whitaker
fix(publish): constrain generated source rewrites (#36109)
20 июл 2026, 11:38
Не верифицирован
20 июл 2026, 11:38
b939e9e
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::path::Path; use std::sync::Arc; use deno_ast::MediaType; use deno_ast::ParsedSource; use deno_ast::SourceTextInfo; use deno_ast::TextChange; use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::url::Url; use deno_graph::ModuleGraph; use deno_resolver::cache::LazyGraphSourceParser; use deno_resolver::cache::ParsedSourceCache; use deno_resolver::deno_json::CompilerOptionsResolver; use deno_resolver::workspace::ResolutionKind; use lazy_regex::Lazy; use super::diagnostics::PublishDiagnostic; use super::diagnostics::PublishDiagnosticsCollector; use super::unfurl::PositionOrSourceRangeRef; use super::unfurl::SpecifierUnfurler; use super::unfurl::SpecifierUnfurlerDiagnostic; use super::unfurl::SpecifierUnfurlerSys; use crate::sys::CliSys; use crate::tools::unfurl_utils::is_safe_unquoted_comment_value; fn jsx_pragma(name: &str, value: &str) -> Result<String, AnyError> { if !is_safe_unquoted_comment_value(value) { return Err(deno_core::anyhow::anyhow!( "Cannot represent compiler option '{name}' as a generated JSX pragma." )); } Ok(format!("/** @{name} {value} */")) } struct JsxFolderOptions<'a> { jsx_runtime: &'static str, jsx_classic: Option<Cow<'a, deno_ast::JsxClassicOptions>>, jsx_import_source: Option<String>, jsx_import_source_types: Option<String>, } #[sys_traits::auto_impl] pub trait ModuleContentProviderSys: SpecifierUnfurlerSys {} pub struct ModuleContentProvider<TSys: ModuleContentProviderSys = CliSys> { specifier_unfurler: SpecifierUnfurler<TSys>, parsed_source_cache: Arc<ParsedSourceCache>, sys: TSys, compiler_options_resolver: Arc<CompilerOptionsResolver>, } impl<TSys: ModuleContentProviderSys> ModuleContentProvider<TSys> { pub fn new( parsed_source_cache: Arc<ParsedSourceCache>, specifier_unfurler: SpecifierUnfurler<TSys>, sys: TSys, compiler_options_resolver: Arc<CompilerOptionsResolver>, ) -> Self { Self { specifier_unfurler, parsed_source_cache, sys, compiler_options_resolver, } } pub fn resolve_content_maybe_unfurling( &self, graph: &ModuleGraph, diagnostics_collector: &PublishDiagnosticsCollector, path: &Path, specifier: &Url, ) -> Result<Vec<u8>, AnyError> { let source_parser = LazyGraphSourceParser::new(&self.parsed_source_cache, graph); let media_type = MediaType::from_specifier(specifier); let parsed_source = match source_parser.get_or_parse_source(specifier)? { Some(parsed_source) => parsed_source, None => { let data = self.sys.fs_read(path).with_context(|| { format!("Unable to read file '{}'", path.display()) })?; match media_type { MediaType::JavaScript | MediaType::Jsx | MediaType::Mjs | MediaType::Cjs | MediaType::TypeScript | MediaType::Mts | MediaType::Cts | MediaType::Dts | MediaType::Dmts | MediaType::Dcts | MediaType::Tsx => { // continue } MediaType::Wasm => { return self.unfurl_wasm(specifier, &data, diagnostics_collector); } MediaType::SourceMap | MediaType::Unknown | MediaType::Html | MediaType::Markdown | MediaType::Sql | MediaType::Json | MediaType::Jsonc | MediaType::Json5 | MediaType::Css => { // not unfurlable data return Ok(data.into_owned()); } } let text = String::from_utf8_lossy(&data); deno_ast::parse_module(deno_ast::ParseParams { specifier: specifier.clone(), text: text.into(), media_type, capture_tokens: false, maybe_syntax: None, scope_analysis: false, })? } }; log::debug!("Unfurling {}", specifier); let mut reporter = |diagnostic| { diagnostics_collector .push(PublishDiagnostic::SpecifierUnfurl(diagnostic)); }; let text_info = parsed_source.text_info_lazy(); let module_info = deno_graph::ast::ParserModuleAnalyzer::module_info(&parsed_source); let mut text_changes = Vec::new(); if media_type.is_jsx() { self.add_jsx_text_changes( specifier, &parsed_source, text_info, &module_info, &mut reporter, &mut text_changes, )?; } self.specifier_unfurler.unfurl_to_changes( specifier, &parsed_source, &module_info, &mut text_changes, &mut reporter, ); let rewritten_text = deno_ast::apply_text_changes(text_info.text_str(), text_changes); Ok(rewritten_text.into_bytes()) } /// Unfurls the module specifiers found in the import section of a Wasm /// module. See [`super::wasm::unfurl_wasm`]. fn unfurl_wasm( &self, specifier: &Url, data: &[u8], diagnostics_collector: &PublishDiagnosticsCollector, ) -> Result<Vec<u8>, AnyError> { log::debug!("Unfurling {}", specifier); let mut reporter = |diagnostic| { diagnostics_collector .push(PublishDiagnostic::SpecifierUnfurl(diagnostic)); }; // Wasm modules are binary, so there is no source text to point diagnostics // at. Use an empty text info with a zeroed range so any diagnostics report // the referrer without a (meaningless) code frame. let text_info = SourceTextInfo::from_string(String::new()); let zeroed_range = deno_graph::PositionRange::zeroed(); super::wasm::unfurl_wasm(data, &mut |module_specifier| { self .specifier_unfurler .unfurl_specifier_reporting_diagnostic( specifier, module_specifier, ResolutionKind::Execution, &text_info, PositionOrSourceRangeRef::PositionRange(&zeroed_range), &mut reporter, ) }) } fn add_jsx_text_changes( &self, specifier: &Url, parsed_source: &ParsedSource, text_info: &SourceTextInfo, module_info: &deno_graph::analysis::ModuleInfo, diagnostic_reporter: &mut dyn FnMut(SpecifierUnfurlerDiagnostic), text_changes: &mut Vec<TextChange>, ) -> Result<(), AnyError> { static JSX_RUNTIME_RE: Lazy<regex::Regex> = lazy_regex::lazy_regex!(r"(?i)^[\s*]*@jsxRuntime\s+(\S+)"); static JSX_FACTORY_RE: Lazy<regex::Regex> = lazy_regex::lazy_regex!(r"(?i)^[\s*]*@jsxFactory\s+(\S+)"); static JSX_FRAGMENT_FACTORY_RE: Lazy<regex::Regex> = lazy_regex::lazy_regex!(r"(?i)^[\s*]*@jsxFragmentFactory\s+(\S+)"); let start_pos = if parsed_source.program_ref().shebang().is_some() { match text_info.text_str().find('\n') { Some(index) => index + 1, None => return Ok(()), // nothing in this file } } else { 0 }; let mut add_text_change = |new_text: String| { text_changes.push(TextChange { range: start_pos..start_pos, new_text, }) }; let jsx_options = self.resolve_jsx_options(specifier, text_info, diagnostic_reporter)?; let leading_comments = parsed_source.get_leading_comments(); let leading_comments_has_re = |regex: ®ex::Regex| { leading_comments .as_ref() .map(|comments| { comments.iter().any(|c| { c.kind == deno_ast::swc::common::comments::CommentKind::Block && regex.is_match(c.text.as_str()) }) }) .unwrap_or(false) }; if !leading_comments_has_re(&JSX_RUNTIME_RE) { add_text_change(format!( "/** @jsxRuntime {} */", jsx_options.jsx_runtime, )); } if module_info.jsx_import_source.is_none() && let Some(import_source) = jsx_options.jsx_import_source { add_text_change(jsx_pragma("jsxImportSource", &import_source)?); } if module_info.jsx_import_source_types.is_none() && let Some(import_source) = jsx_options.jsx_import_source_types { add_text_change(jsx_pragma("jsxImportSourceTypes", &import_source)?); } if let Some(classic_options) = &jsx_options.jsx_classic { if !leading_comments_has_re(&JSX_FACTORY_RE) { add_text_change(jsx_pragma("jsxFactory", &classic_options.factory)?); } if !leading_comments_has_re(&JSX_FRAGMENT_FACTORY_RE) { add_text_change(jsx_pragma( "jsxFragmentFactory", &classic_options.fragment_factory, )?); } } Ok(()) } fn resolve_jsx_options<'a>( &'a self, specifier: &Url, text_info: &SourceTextInfo, diagnostic_reporter: &mut dyn FnMut(SpecifierUnfurlerDiagnostic), ) -> Result<JsxFolderOptions<'a>, AnyError> { let compiler_options = self.compiler_options_resolver.for_specifier(specifier); let jsx_config = compiler_options.jsx_import_source_config()?; let transpile_options = &compiler_options.transpile_options()?.transpile; let jsx_runtime = match &transpile_options.jsx { Some( deno_ast::JsxRuntime::Automatic(_) | deno_ast::JsxRuntime::Precompile(_), ) => "automatic", None | Some(deno_ast::JsxRuntime::Classic(_)) => "classic", }; let mut unfurl_import_source = |import_source: &str, referrer: &Url, resolution_kind: ResolutionKind| { let maybe_import_source = self .specifier_unfurler .unfurl_specifier_reporting_diagnostic( referrer, import_source, resolution_kind, text_info, PositionOrSourceRangeRef::PositionRange( &deno_graph::PositionRange::zeroed(), ), diagnostic_reporter, ); maybe_import_source.unwrap_or_else(|| import_source.to_string()) }; let jsx_import_source = jsx_config .and_then(|c| c.import_source.as_ref()) .map(|jsx_import_source| { unfurl_import_source( &jsx_import_source.specifier, &jsx_import_source.base, ResolutionKind::Execution, ) }); let jsx_import_source_types = jsx_config .and_then(|c| c.import_source_types.as_ref()) .map(|jsx_import_source_types| { unfurl_import_source( &jsx_import_source_types.specifier, &jsx_import_source_types.base, ResolutionKind::Types, ) }); let classic_options = match &transpile_options.jsx { None => Some(Cow::Owned(deno_ast::JsxClassicOptions::default())), Some(deno_ast::JsxRuntime::Classic(classic_options)) => { Some(Cow::Borrowed(classic_options)) } Some( deno_ast::JsxRuntime::Precompile(_) | deno_ast::JsxRuntime::Automatic(_), ) => None, }; Ok(JsxFolderOptions { jsx_runtime, jsx_classic: classic_options, jsx_import_source, jsx_import_source_types, }) } } #[cfg(test)] mod test { use std::path::PathBuf; use deno_path_util::url_from_file_path; use deno_resolver::factory::ResolverFactory; use deno_resolver::factory::ResolverFactoryOptions; use deno_resolver::factory::WorkspaceFactory; use deno_resolver::factory::WorkspaceFactoryOptions; use pretty_assertions::assert_eq; use sys_traits::FsCreateDirAll; use sys_traits::FsWrite; use sys_traits::impls::InMemorySys; use super::*; #[test] fn test_jsx_pragma_safety() { assert_eq!( jsx_pragma("jsxImportSource", "npm:react").unwrap(), "/** @jsxImportSource npm:react */" ); for value in [ "", "npm:package/sub*/path", "npm:package/with space", "line\nbreak", ] { assert!(jsx_pragma("jsxImportSource", value).is_err()); } } #[tokio::test] async fn test_module_content_jsx() { run_test(&[ ( "/deno.json", r#"{ "nodeModulesDir": "manual", "workspace": ["package-a", "package-b", "package-c", "package-d"] }"#, None, ), ( "/package-a/deno.json", r#"{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "react", "jsxImportSourceTypes": "@types/react", }, "imports": { "react": "npm:react" "@types/react": "npm:@types/react" } }"#, None, ), ( "/package-b/deno.json", r#"{ "compilerOptions": { "jsx": "react-jsx" }, "imports": { "react": "npm:react" "@types/react": "npm:@types/react" } }"#, None, ), ( "/package-c/deno.json", r#"{ "compilerOptions": { "jsx": "precompile", "jsxImportSource": "react", "jsxImportSourceTypes": "@types/react", }, "imports": { "react": "npm:react" "@types/react": "npm:@types/react" } }"#, None, ), ( "/package-d/deno.json", r#"{ "compilerOptions": { "jsx": "react" }, "imports": { "react": "npm:react" "@types/react": "npm:@types/react" } }"#, None, ), ( "/package-a/main.tsx", "export const component = <div></div>;", Some( "/** @jsxRuntime automatic *//** @jsxImportSource npm:react *//** @jsxImportSourceTypes npm:@types/react */export const component = <div></div>;", ), ), ( "/package-b/main.tsx", "export const componentB = <div></div>;", Some( "/** @jsxRuntime automatic *//** @jsxImportSource npm:react *//** @jsxImportSourceTypes npm:react */export const componentB = <div></div>;", ), ), ( "/package-a/other.tsx", "/** @jsxImportSource npm:preact */ /** @jsxFragmentFactory h1 */ /** @jsxImportSourceTypes npm:@types/example */ /** @jsxFactory h2 */ /** @jsxRuntime automatic */ export const component = <div></div>;", Some( "/** @jsxImportSource npm:preact */ /** @jsxFragmentFactory h1 */ /** @jsxImportSourceTypes npm:@types/example */ /** @jsxFactory h2 */ /** @jsxRuntime automatic */ export const component = <div></div>;", ), ), ( "/package-c/main.tsx", "export const component = <div></div>;", Some( "/** @jsxRuntime automatic *//** @jsxImportSource npm:react *//** @jsxImportSourceTypes npm:@types/react */export const component = <div></div>;", ), ), ( "/package-d/main.tsx", "export const component = <div></div>;", Some( "/** @jsxRuntime classic *//** @jsxFactory React.createElement *//** @jsxFragmentFactory React.Fragment */export const component = <div></div>;", ), ), ]).await; } #[tokio::test] async fn test_module_content_rejects_unrepresentable_jsx_pragma() { let in_memory_sys = InMemorySys::default(); in_memory_sys.fs_create_dir_all(get_path("/")).unwrap(); in_memory_sys .fs_write( get_path("/deno.json"), r#"{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "npm:package/sub*/path" } }"#, ) .unwrap(); in_memory_sys .fs_write( get_path("/main.tsx"), "export const component = <div></div>;", ) .unwrap(); let provider = module_content_provider(in_memory_sys).await; let path = get_path("/main.tsx"); let error = provider .resolve_content_maybe_unfurling( &ModuleGraph::new(deno_graph::GraphKind::All), &Default::default(), &path, &url_from_file_path(&path).unwrap(), ) .unwrap_err(); assert!( error .to_string() .contains("Cannot represent compiler option 'jsxImportSource'") ); } #[tokio::test] async fn test_module_content_wasm() { let in_memory_sys = InMemorySys::default(); in_memory_sys.fs_create_dir_all(get_path("/")).unwrap(); in_memory_sys .fs_write( get_path("/deno.json"), r#"{ "name": "@scope/pkg", "version": "1.0.0", "exports": "./main.wasm", "nodeModulesDir": "manual", "imports": { "@std/foo": "jsr:@std/foo@1", "chalk": "npm:chalk@5" } }"#, ) .unwrap(); // A Wasm module importing a bare specifier (mapped via the import map), an // npm specifier (mapped via the import map) and a relative specifier (left // as-is). let wasm = build_wasm_with_imports(&["@std/foo", "chalk", "./other.js"]); in_memory_sys .fs_write(get_path("/main.wasm"), &wasm) .unwrap(); let provider = module_content_provider(in_memory_sys).await; let path = get_path("/main.wasm"); let bytes = provider .resolve_content_maybe_unfurling( &ModuleGraph::new(deno_graph::GraphKind::All), &Default::default(), &path, &url_from_file_path(&path).unwrap(), ) .unwrap(); assert_eq!( wasm_import_modules(&bytes), vec![ "jsr:@std/foo@1".to_string(), "npm:chalk@5".to_string(), "./other.js".to_string(), ] ); } fn build_wasm_with_imports(modules: &[&str]) -> Vec<u8> { fn write_var_u32(mut value: u32, output: &mut Vec<u8>) { loop { let mut byte = (value & 0x7f) as u8; value >>= 7; if value != 0 { byte |= 0x80; } output.push(byte); if value == 0 { break; } } } fn write_wasm_string(value: &str, output: &mut Vec<u8>) { write_var_u32(value.len() as u32, output); output.extend_from_slice(value.as_bytes()); } fn section(id: u8, body: &[u8], output: &mut Vec<u8>) { output.push(id); write_var_u32(body.len() as u32, output); output.extend_from_slice(body); } let mut wasm = b"\0asm\x01\0\0\0".to_vec(); let types = [0x01, 0x60, 0x00, 0x00]; section(1, &types, &mut wasm); let mut imports = Vec::new(); write_var_u32(modules.len() as u32, &mut imports); for (i, module) in modules.iter().enumerate() { write_wasm_string(module, &mut imports); write_wasm_string(&format!("import_{i}"), &mut imports); imports.extend_from_slice(&[0x00, 0x00]); } section(2, &imports, &mut wasm); wasm } fn wasm_import_modules(bytes: &[u8]) -> Vec<String> { let mut modules = Vec::new(); for payload in wasmparser::Parser::new(0).parse_all(bytes) { if let wasmparser::Payload::ImportSection(reader) = payload.unwrap() { for import in reader.into_imports() { modules.push(import.unwrap().module.to_string()); } } } modules } fn get_path(path: &str) -> PathBuf { PathBuf::from(if cfg!(windows) { format!("C:{}", path.replace('/', "\\")) } else { path.to_string() }) } async fn run_test( files: &[(&'static str, &'static str, Option<&'static str>)], ) { let in_memory_sys = InMemorySys::default(); for (path, text, _) in files { let path = get_path(path); in_memory_sys .fs_create_dir_all(path.parent().unwrap()) .unwrap(); in_memory_sys.fs_write(path, text).unwrap(); } let provider = module_content_provider(in_memory_sys).await; for (path, _, expected) in files { let Some(expected) = expected else { continue; }; let path = get_path(path); let bytes = provider .resolve_content_maybe_unfurling( &ModuleGraph::new(deno_graph::GraphKind::All), &Default::default(), &path, &url_from_file_path(&path).unwrap(), ) .unwrap(); assert_eq!(String::from_utf8_lossy(&bytes), *expected); } } async fn module_content_provider( sys: InMemorySys, ) -> ModuleContentProvider<InMemorySys> { let cwd = get_path("/"); let workspace_factory = Arc::new(WorkspaceFactory::new( sys.clone(), cwd.to_path_buf(), WorkspaceFactoryOptions { maybe_custom_deno_dir_root: Some(cwd.join("deno_dir")), ..Default::default() }, )); let resolver_factory = ResolverFactory::new( workspace_factory, ResolverFactoryOptions { package_json_dep_resolution: Some( deno_resolver::workspace::PackageJsonDepResolution::Enabled, ), unstable_sloppy_imports: true, ..Default::default() }, ); let specifier_unfurler = SpecifierUnfurler::new( resolver_factory.node_resolver().unwrap().clone(), resolver_factory.npm_req_resolver().unwrap().clone(), resolver_factory.pkg_json_resolver().clone(), resolver_factory .workspace_factory() .workspace_directory() .unwrap() .clone(), resolver_factory.workspace_resolver().await.unwrap().clone(), ); ModuleContentProvider::new( Arc::new(ParsedSourceCache::default()), specifier_unfurler, sys, resolver_factory .compiler_options_resolver() .unwrap() .clone(), ) } }