diff --git a/Cargo.lock b/Cargo.lock index ffad8b61d6..61e3607aa4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -782,9 +782,9 @@ dependencies = [ [[package]] name = "deno_doc" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08abadd9f3ede74c5ba6e3d9a688ecfe160cf7fb2988ae133ef4e3d591d091e7" +checksum = "6d2d76b6b75a6fbfda0f529e310fc3cab960f4219403280b430ce93dcf8cf9a2" dependencies = [ "cfg-if 1.0.0", "deno_ast", @@ -827,9 +827,9 @@ dependencies = [ [[package]] name = "deno_graph" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6df7e1b135780d9424ce4fb9a8927983d27d2c094922cb84b5fd5d72a4c85b82" +checksum = "f6d84ddee0cf83bf295721be792b6769b92214983bda29d52c2b05a89d1e968f" dependencies = [ "anyhow", "cfg-if 1.0.0", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 32527180b9..0fe47934d0 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -41,8 +41,8 @@ winres = "0.1.11" [dependencies] deno_ast = { version = "0.5.0", features = ["bundler", "codegen", "dep_graph", "module_specifier", "proposal", "react", "sourcemap", "transforms", "typescript", "view", "visit"] } deno_core = { version = "0.105.0", path = "../core" } -deno_doc = "0.19.0" -deno_graph = "0.10.0" +deno_doc = "0.20.0" +deno_graph = "0.11.1" deno_lint = { version = "0.19.0", features = ["docs"] } deno_runtime = { version = "0.31.0", path = "../runtime" } deno_tls = { version = "0.10.0", path = "../ext/tls" } diff --git a/cli/ast/mod.rs b/cli/ast/mod.rs index 626e75b454..e3722c149d 100644 --- a/cli/ast/mod.rs +++ b/cli/ast/mod.rs @@ -122,15 +122,26 @@ pub struct EmitOptions { pub inline_source_map: bool, /// Should the sources be inlined in the source map. Defaults to `true`. pub inline_sources: bool, - // Should a corresponding .map file be created for the output. This should be - // false if inline_source_map is true. Defaults to `false`. + /// Should a corresponding .map file be created for the output. This should be + /// false if inline_source_map is true. Defaults to `false`. pub source_map: bool, + /// `true` if the program should use an implicit JSX import source/the "new" + /// JSX transforms. + pub jsx_automatic: bool, + /// If JSX is automatic, if it is in development mode, meaning that it should + /// import `jsx-dev-runtime` and transform JSX using `jsxDEV` import from the + /// JSX import source as well as provide additional debug information to the + /// JSX factory. + pub jsx_development: bool, /// When transforming JSX, what value should be used for the JSX factory. /// Defaults to `React.createElement`. pub jsx_factory: String, /// When transforming JSX, what value should be used for the JSX fragment /// factory. Defaults to `React.Fragment`. pub jsx_fragment_factory: String, + /// The string module specifier to implicitly import JSX factories from when + /// transpiling JSX. + pub jsx_import_source: Option, /// Should JSX be transformed or preserved. Defaults to `true`. pub transform_jsx: bool, /// Should import declarations be transformed to variable declarations. @@ -146,8 +157,11 @@ impl Default for EmitOptions { inline_source_map: true, inline_sources: true, source_map: false, + jsx_automatic: false, + jsx_development: false, jsx_factory: "React.createElement".into(), jsx_fragment_factory: "React.Fragment".into(), + jsx_import_source: None, transform_jsx: true, repl_imports: false, } @@ -164,15 +178,25 @@ impl From for EmitOptions { "error" => ImportsNotUsedAsValues::Error, _ => ImportsNotUsedAsValues::Remove, }; + let (transform_jsx, jsx_automatic, jsx_development) = + match options.jsx.as_str() { + "react" => (true, false, false), + "react-jsx" => (true, true, false), + "react-jsxdev" => (true, true, true), + _ => (false, false, false), + }; EmitOptions { emit_metadata: options.emit_decorator_metadata, imports_not_used_as_values, inline_source_map: options.inline_source_map, inline_sources: options.inline_sources, source_map: options.source_map, + jsx_automatic, + jsx_development, jsx_factory: options.jsx_factory, jsx_fragment_factory: options.jsx_fragment_factory, - transform_jsx: options.jsx == "react", + jsx_import_source: options.jsx_import_source, + transform_jsx, repl_imports: false, } } @@ -355,6 +379,13 @@ fn fold_program( // this will use `Object.assign()` instead of the `_extends` helper // when spreading props. use_builtins: true, + runtime: if options.jsx_automatic { + Some(react::Runtime::Automatic) + } else { + None + }, + development: options.jsx_development, + import_source: options.jsx_import_source.clone().unwrap_or_default(), ..Default::default() }, top_level_mark, @@ -495,6 +526,112 @@ function App() { assert_eq!(&code[..expected.len()], expected); } + #[test] + fn test_transpile_jsx_import_source_pragma() { + let specifier = resolve_url_or_path("https://deno.land/x/mod.tsx") + .expect("could not resolve specifier"); + let source = r#" +/** @jsxImportSource jsx_lib */ + +function App() { + return ( +
<>
+ ); +}"#; + let module = parse_module(ParseParams { + specifier: specifier.as_str().to_string(), + source: SourceTextInfo::from_string(source.to_string()), + media_type: deno_ast::MediaType::Jsx, + capture_tokens: false, + maybe_syntax: None, + scope_analysis: true, + }) + .unwrap(); + let (code, _) = transpile(&module, &EmitOptions::default()).unwrap(); + let expected = r#"import { jsx as _jsx, Fragment as _Fragment } from "jsx_lib/jsx-runtime"; +/** @jsxImportSource jsx_lib */ function App() { + return(/*#__PURE__*/ _jsx("div", { + children: /*#__PURE__*/ _jsx(_Fragment, { + }) + })); +"#; + assert_eq!(&code[..expected.len()], expected); + } + + #[test] + fn test_transpile_jsx_import_source_no_pragma() { + let specifier = resolve_url_or_path("https://deno.land/x/mod.tsx") + .expect("could not resolve specifier"); + let source = r#" +function App() { + return ( +
<>
+ ); +}"#; + let module = parse_module(ParseParams { + specifier: specifier.as_str().to_string(), + source: SourceTextInfo::from_string(source.to_string()), + media_type: deno_ast::MediaType::Jsx, + capture_tokens: false, + maybe_syntax: None, + scope_analysis: true, + }) + .unwrap(); + let emit_options = EmitOptions { + jsx_automatic: true, + jsx_import_source: Some("jsx_lib".to_string()), + ..Default::default() + }; + let (code, _) = transpile(&module, &emit_options).unwrap(); + let expected = r#"import { jsx as _jsx, Fragment as _Fragment } from "jsx_lib/jsx-runtime"; +function App() { + return(/*#__PURE__*/ _jsx("div", { + children: /*#__PURE__*/ _jsx(_Fragment, { + }) + })); +} +"#; + assert_eq!(&code[..expected.len()], expected); + } + + // TODO(@kitsonk) https://github.com/swc-project/swc/issues/2656 + // #[test] + // fn test_transpile_jsx_import_source_no_pragma_dev() { + // let specifier = resolve_url_or_path("https://deno.land/x/mod.tsx") + // .expect("could not resolve specifier"); + // let source = r#" + // function App() { + // return ( + //
<>
+ // ); + // }"#; + // let module = parse_module(ParseParams { + // specifier: specifier.as_str().to_string(), + // source: SourceTextInfo::from_string(source.to_string()), + // media_type: deno_ast::MediaType::Jsx, + // capture_tokens: false, + // maybe_syntax: None, + // scope_analysis: true, + // }) + // .unwrap(); + // let emit_options = EmitOptions { + // jsx_automatic: true, + // jsx_import_source: Some("jsx_lib".to_string()), + // jsx_development: true, + // ..Default::default() + // }; + // let (code, _) = transpile(&module, &emit_options).unwrap(); + // let expected = r#"import { jsx as _jsx, Fragment as _Fragment } from "jsx_lib/jsx-dev-runtime"; + // function App() { + // return(/*#__PURE__*/ _jsx("div", { + // children: /*#__PURE__*/ _jsx(_Fragment, { + // }) + // })); + // } + // "#; + // assert_eq!(&code[..expected.len()], expected); + // } + #[test] fn test_transpile_decorators() { let specifier = resolve_url_or_path("https://deno.land/x/mod.ts") diff --git a/cli/compat/esm_resolver.rs b/cli/compat/esm_resolver.rs index 0709c66a3b..257a52f3e7 100644 --- a/cli/compat/esm_resolver.rs +++ b/cli/compat/esm_resolver.rs @@ -14,12 +14,12 @@ use regex::Regex; use std::path::PathBuf; #[derive(Debug, Default)] -pub(crate) struct NodeEsmResolver<'a> { - maybe_import_map_resolver: Option>, +pub(crate) struct NodeEsmResolver { + maybe_import_map_resolver: Option, } -impl<'a> NodeEsmResolver<'a> { - pub fn new(maybe_import_map_resolver: Option>) -> Self { +impl NodeEsmResolver { + pub fn new(maybe_import_map_resolver: Option) -> Self { Self { maybe_import_map_resolver, } @@ -30,7 +30,7 @@ impl<'a> NodeEsmResolver<'a> { } } -impl Resolver for NodeEsmResolver<'_> { +impl Resolver for NodeEsmResolver { fn resolve( &self, specifier: &str, diff --git a/cli/config_file.rs b/cli/config_file.rs index 3a71d41a92..20e254dd0d 100644 --- a/cli/config_file.rs +++ b/cli/config_file.rs @@ -1,7 +1,9 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. use crate::fs_util::canonicalize_path; + use deno_core::error::anyhow; +use deno_core::error::custom_error; use deno_core::error::AnyError; use deno_core::error::Context; use deno_core::serde::Deserialize; @@ -17,6 +19,9 @@ use std::fmt; use std::path::Path; use std::path::PathBuf; +pub(crate) type MaybeImportsResult = + Result)>>, AnyError>; + /// The transpile options that are significant out of a user provided tsconfig /// file, that we want to deserialize out of the final config for a transpile. #[derive(Debug, Deserialize)] @@ -31,6 +36,7 @@ pub struct EmitConfigOptions { pub jsx: String, pub jsx_factory: String, pub jsx_fragment_factory: String, + pub jsx_import_source: Option, } /// There are certain compiler options that can impact what modules are part of @@ -38,6 +44,8 @@ pub struct EmitConfigOptions { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CompilerOptions { + pub jsx: Option, + pub jsx_import_source: Option, pub types: Option>, } @@ -404,15 +412,50 @@ impl ConfigFile { /// If the configuration file contains "extra" modules (like TypeScript /// `"types"`) options, return them as imports to be added to a module graph. - pub fn to_maybe_imports( - &self, - ) -> Option)>> { + pub fn to_maybe_imports(&self) -> MaybeImportsResult { + let mut imports = Vec::new(); + let compiler_options_value = + if let Some(value) = self.json.compiler_options.as_ref() { + value + } else { + return Ok(None); + }; + let compiler_options: CompilerOptions = + serde_json::from_value(compiler_options_value.clone())?; + let referrer = ModuleSpecifier::from_file_path(&self.path) + .map_err(|_| custom_error("TypeError", "bad config file specifier"))?; + if let Some(types) = compiler_options.types { + imports.extend(types); + } + if compiler_options.jsx == Some("react-jsx".to_string()) { + imports.push(format!( + "{}/jsx-runtime", + compiler_options.jsx_import_source.ok_or_else(|| custom_error("TypeError", "Compiler option 'jsx' set to 'react-jsx', but no 'jsxImportSource' defined."))? + )); + } else if compiler_options.jsx == Some("react-jsxdev".to_string()) { + imports.push(format!( + "{}/jsx-dev-runtime", + compiler_options.jsx_import_source.ok_or_else(|| custom_error("TypeError", "Compiler option 'jsx' set to 'react-jsxdev', but no 'jsxImportSource' defined."))? + )); + } + if !imports.is_empty() { + Ok(Some(vec![(referrer, imports)])) + } else { + Ok(None) + } + } + + /// Based on the compiler options in the configuration file, return the + /// implied JSX import source module. + pub fn to_maybe_jsx_import_source_module(&self) -> Option { let compiler_options_value = self.json.compiler_options.as_ref()?; let compiler_options: CompilerOptions = serde_json::from_value(compiler_options_value.clone()).ok()?; - let referrer = ModuleSpecifier::from_file_path(&self.path).ok()?; - let types = compiler_options.types?; - Some(vec![(referrer, types)]) + match compiler_options.jsx.as_deref() { + Some("react-jsx") => Some("jsx-runtime".to_string()), + Some("react-jsxdev") => Some("jsx-dev-runtime".to_string()), + _ => None, + } } pub fn to_fmt_config(&self) -> Result, AnyError> { diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index 116b510f0b..ddf597a0ab 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -277,15 +277,20 @@ declare namespace Deno { /** Emit the source alongside the source maps within a single file; requires * `inlineSourceMap` or `sourceMap` to be set. Defaults to `false`. */ inlineSources?: boolean; - /** Support JSX in `.tsx` files: `"react"`, `"preserve"`, `"react-native"`. + /** Support JSX in `.tsx` files: `"react"`, `"preserve"`, `"react-native"`, + * `"react-jsx", `"react-jsxdev"`. * Defaults to `"react"`. */ - jsx?: "react" | "preserve" | "react-native"; + jsx?: "react" | "preserve" | "react-native" | "react-jsx" | "react-jsx-dev"; /** Specify the JSX factory function to use when targeting react JSX emit, * e.g. `React.createElement` or `h`. Defaults to `React.createElement`. */ jsxFactory?: string; /** Specify the JSX fragment factory function to use when targeting react * JSX emit, e.g. `Fragment`. Defaults to `React.Fragment`. */ jsxFragmentFactory?: string; + /** Declares the module specifier to be used for importing the `jsx` and + * `jsxs` factory functions when using jsx as `"react-jsx"` or + * `"react-jsxdev"`. Defaults to `"react"`. */ + jsxImportSource?: string; /** Resolve keyof to string valued property names only (no numbers or * symbols). Defaults to `false`. */ keyofStringsOnly?: string; diff --git a/cli/http_util.rs b/cli/http_util.rs index 521acadfa7..1a2aaf0d50 100644 --- a/cli/http_util.rs +++ b/cli/http_util.rs @@ -1,6 +1,7 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. use crate::auth_tokens::AuthToken; +use deno_core::error::custom_error; use deno_core::error::generic_error; use deno_core::error::AnyError; use deno_core::url::Url; @@ -123,11 +124,18 @@ pub async fn fetch_once( if response.status().is_client_error() || response.status().is_server_error() { - let err = generic_error(format!( - "Import '{}' failed: {}", - args.url, - response.status() - )); + let err = if response.status() == StatusCode::NOT_FOUND { + custom_error( + "NotFound", + format!("Import '{}' failed, not found.", args.url), + ) + } else { + generic_error(format!( + "Import '{}' failed: {}", + args.url, + response.status() + )) + }; return Err(err); } diff --git a/cli/lsp/cache.rs b/cli/lsp/cache.rs index 1ea3172402..b7bdb90c59 100644 --- a/cli/lsp/cache.rs +++ b/cli/lsp/cache.rs @@ -2,9 +2,11 @@ use crate::cache::CacherLoader; use crate::cache::FetchCacher; +use crate::config_file::ConfigFile; use crate::flags::Flags; use crate::proc_state::ProcState; use crate::resolver::ImportMapResolver; +use crate::resolver::JsxResolver; use deno_core::error::anyhow; use deno_core::error::AnyError; @@ -13,6 +15,7 @@ use deno_runtime::permissions::Permissions; use deno_runtime::tokio_util::create_basic_runtime; use import_map::ImportMap; use std::path::PathBuf; +use std::sync::Arc; use std::thread; use tokio::sync::mpsc; use tokio::sync::oneshot; @@ -27,7 +30,8 @@ pub(crate) struct CacheServer(mpsc::UnboundedSender); impl CacheServer { pub async fn new( maybe_cache_path: Option, - maybe_import_map: Option, + maybe_import_map: Option>, + maybe_config_file: Option, ) -> Self { let (tx, mut rx) = mpsc::unbounded_channel::(); let _join_handle = thread::spawn(move || { @@ -39,8 +43,26 @@ impl CacheServer { }) .await .unwrap(); - let maybe_resolver = - maybe_import_map.as_ref().map(ImportMapResolver::new); + let maybe_import_map_resolver = + maybe_import_map.map(ImportMapResolver::new); + let maybe_jsx_resolver = maybe_config_file + .as_ref() + .map(|cf| { + cf.to_maybe_jsx_import_source_module() + .map(|im| JsxResolver::new(im, maybe_import_map_resolver.clone())) + }) + .flatten(); + let maybe_resolver = if maybe_jsx_resolver.is_some() { + maybe_jsx_resolver.as_ref().map(|jr| jr.as_resolver()) + } else { + maybe_import_map_resolver + .as_ref() + .map(|im| im.as_resolver()) + }; + let maybe_imports = maybe_config_file + .map(|cf| cf.to_maybe_imports().ok()) + .flatten() + .flatten(); let mut cache = FetchCacher::new( ps.dir.gen_cache.clone(), ps.file_fetcher.clone(), @@ -52,9 +74,9 @@ impl CacheServer { let graph = deno_graph::create_graph( roots, false, - None, + maybe_imports.clone(), cache.as_mut_loader(), - maybe_resolver.as_ref().map(|r| r.as_resolver()), + maybe_resolver, None, None, ) diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index dc5b0f0044..ce7e4e36f7 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -1,14 +1,16 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -use super::resolver::ImportMapResolver; use super::text::LineIndex; use super::tsc; +use crate::config_file::ConfigFile; use crate::file_fetcher::get_source_from_bytes; use crate::file_fetcher::map_content_type; use crate::file_fetcher::SUPPORTED_SCHEMES; use crate::http_cache; use crate::http_cache::HttpCache; +use crate::resolver::ImportMapResolver; +use crate::resolver::JsxResolver; use crate::text_encoding; use deno_ast::MediaType; @@ -19,6 +21,7 @@ use deno_core::parking_lot::Mutex; use deno_core::url; use deno_core::ModuleSpecifier; use lspower::lsp; +use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::fs; @@ -131,6 +134,59 @@ impl IndexValid { } } +// TODO(@kitsonk) expose the synthetic module from deno_graph +#[derive(Debug)] +struct SyntheticModule { + dependencies: BTreeMap, + specifier: ModuleSpecifier, +} + +impl SyntheticModule { + pub fn new( + specifier: ModuleSpecifier, + dependencies: Vec<(String, Option)>, + maybe_resolver: Option<&dyn deno_graph::source::Resolver>, + ) -> Self { + let dependencies = dependencies + .iter() + .map(|(dep, maybe_range)| { + let range = to_deno_graph_range(&specifier, maybe_range.as_ref()); + let result = if let Some(resolver) = maybe_resolver { + resolver.resolve(dep, &specifier).map_err(|err| { + if let Some(specifier_error) = + err.downcast_ref::() + { + deno_graph::ResolutionError::InvalidSpecifier( + specifier_error.clone(), + range.clone(), + ) + } else { + deno_graph::ResolutionError::ResolverError( + Arc::new(err), + dep.to_string(), + range.clone(), + ) + } + }) + } else { + deno_core::resolve_import(dep, specifier.as_str()).map_err(|err| { + deno_graph::ResolutionError::ResolverError( + Arc::new(err.into()), + dep.to_string(), + range.clone(), + ) + }) + }; + (dep.to_string(), Some(result.map(|s| (s, range)))) + }) + .collect(); + Self { + dependencies, + specifier, + } + } +} + #[derive(Debug)] pub(crate) struct Document { line_index: Arc, @@ -347,6 +403,32 @@ pub(crate) fn to_lsp_range(range: &deno_graph::Range) -> lsp::Range { } } +fn to_deno_graph_range( + specifier: &ModuleSpecifier, + maybe_range: Option<&lsp::Range>, +) -> deno_graph::Range { + let specifier = specifier.clone(); + if let Some(range) = maybe_range { + deno_graph::Range { + specifier, + start: deno_graph::Position { + line: range.start.line as usize, + character: range.start.character as usize, + }, + end: deno_graph::Position { + line: range.end.line as usize, + character: range.end.character as usize, + }, + } + } else { + deno_graph::Range { + specifier, + start: deno_graph::Position::zeroed(), + end: deno_graph::Position::zeroed(), + } + } +} + /// Recurse and collect specifiers that appear in the dependent map. fn recurse_dependents( specifier: &ModuleSpecifier, @@ -376,8 +458,13 @@ struct Inner { /// A map of documents that can either be "open" in the language server, or /// just present on disk. docs: HashMap, + /// Any imports to the context supplied by configuration files. This is like + /// the imports into the a module graph in CLI. + imports: HashMap, /// The optional import map that should be used when resolving dependencies. maybe_import_map: Option, + /// The optional JSX resolver, which is used when JSX imports are configured. + maybe_jsx_resolver: Option, redirects: HashMap, } @@ -388,7 +475,9 @@ impl Inner { dirty: true, dependents_map: HashMap::default(), docs: HashMap::default(), + imports: HashMap::default(), maybe_import_map: None, + maybe_jsx_resolver: None, redirects: HashMap::default(), } } @@ -407,7 +496,7 @@ impl Inner { version, None, content, - self.maybe_import_map.as_ref().map(|r| r.as_resolver()), + self.get_maybe_resolver(), ) } else { let cache_filename = self.cache.get_cache_filename(&specifier)?; @@ -421,7 +510,7 @@ impl Inner { version, maybe_headers, content, - self.maybe_import_map.as_ref().map(|r| r.as_resolver()), + self.get_maybe_resolver(), ) }; self.dirty = true; @@ -481,6 +570,14 @@ impl Inner { version: i32, changes: Vec, ) -> Result<(), AnyError> { + // this duplicates the .get_resolver() method, because there is no easy + // way to avoid the double borrow of self that occurs here with getting the + // mut doc out. + let maybe_resolver = if self.maybe_jsx_resolver.is_some() { + self.maybe_jsx_resolver.as_ref().map(|jr| jr.as_resolver()) + } else { + self.maybe_import_map.as_ref().map(|im| im.as_resolver()) + }; let doc = self.docs.get_mut(specifier).map_or_else( || { Err(custom_error( @@ -491,11 +588,7 @@ impl Inner { Ok, )?; self.dirty = true; - doc.change( - version, - changes, - self.maybe_import_map.as_ref().map(|r| r.as_resolver()), - ) + doc.change(version, changes, maybe_resolver) } fn close(&mut self, specifier: &ModuleSpecifier) -> Result<(), AnyError> { @@ -518,8 +611,7 @@ impl Inner { specifier: &str, referrer: &ModuleSpecifier, ) -> bool { - let maybe_resolver = - self.maybe_import_map.as_ref().map(|im| im.as_resolver()); + let maybe_resolver = self.get_maybe_resolver(); let maybe_specifier = if let Some(resolver) = maybe_resolver { resolver.resolve(specifier, referrer).ok() } else { @@ -604,6 +696,14 @@ impl Inner { }) } + fn get_maybe_resolver(&self) -> Option<&dyn deno_graph::source::Resolver> { + if self.maybe_jsx_resolver.is_some() { + self.maybe_jsx_resolver.as_ref().map(|jr| jr.as_resolver()) + } else { + self.maybe_import_map.as_ref().map(|im| im.as_resolver()) + } + } + fn get_maybe_types_for_dependency( &mut self, dependency: &deno_graph::Dependency, @@ -706,12 +806,13 @@ impl Inner { language_id: LanguageId, content: Arc, ) { + let maybe_resolver = self.get_maybe_resolver(); let document_data = Document::open( specifier.clone(), version, language_id, content, - self.maybe_import_map.as_ref().map(|r| r.as_resolver()), + maybe_resolver, ); self.docs.insert(specifier, document_data); self.dirty = true; @@ -758,6 +859,12 @@ impl Inner { } else { results.push(None); } + } else if let Some(Some(Ok((specifier, _)))) = + self.resolve_imports_dependency(&specifier) + { + // clone here to avoid double borrow of self + let specifier = specifier.clone(); + results.push(self.resolve_dependency(&specifier)); } else { results.push(None); } @@ -790,6 +897,22 @@ impl Inner { } } + /// Iterate through any "imported" modules, checking to see if a dependency + /// is available. This is used to provide "global" imports like the JSX import + /// source. + fn resolve_imports_dependency( + &self, + specifier: &str, + ) -> Option<&deno_graph::Resolved> { + for module in self.imports.values() { + let maybe_dep = module.dependencies.get(specifier); + if maybe_dep.is_some() { + return maybe_dep; + } + } + None + } + fn resolve_remote_specifier( &self, specifier: &ModuleSpecifier, @@ -832,15 +955,6 @@ impl Inner { } } - fn set_import_map( - &mut self, - maybe_import_map: Option>, - ) { - // TODO update resolved dependencies? - self.maybe_import_map = maybe_import_map.map(ImportMapResolver::new); - self.dirty = true; - } - fn set_location(&mut self, location: PathBuf) { // TODO update resolved dependencies? self.cache = HttpCache::new(&location); @@ -886,6 +1000,36 @@ impl Inner { self.get(specifier).map(|d| d.source.clone()) } + fn update_config( + &mut self, + maybe_import_map: Option>, + maybe_config_file: Option<&ConfigFile>, + ) { + // TODO(@kitsonk) update resolved dependencies? + self.maybe_import_map = maybe_import_map.map(ImportMapResolver::new); + self.maybe_jsx_resolver = maybe_config_file + .map(|cf| { + cf.to_maybe_jsx_import_source_module() + .map(|im| JsxResolver::new(im, self.maybe_import_map.clone())) + }) + .flatten(); + if let Some(Ok(Some(imports))) = + maybe_config_file.map(|cf| cf.to_maybe_imports()) + { + for (referrer, dependencies) in imports { + let dependencies = + dependencies.into_iter().map(|s| (s, None)).collect(); + let module = SyntheticModule::new( + referrer.clone(), + dependencies, + self.get_maybe_resolver(), + ); + self.imports.insert(referrer, module); + } + } + self.dirty = true; + } + fn version(&mut self, specifier: &ModuleSpecifier) -> Option { self.get(specifier).map(|d| { d.maybe_lsp_version @@ -1050,14 +1194,6 @@ impl Documents { self.0.lock().resolve(specifiers, referrer) } - /// Set the optional import map for the document cache. - pub fn set_import_map( - &self, - maybe_import_map: Option>, - ) { - self.0.lock().set_import_map(maybe_import_map); - } - /// Update the location of the on disk cache for the document store. pub fn set_location(&self, location: PathBuf) { self.0.lock().set_location(location) @@ -1095,6 +1231,17 @@ impl Documents { self.0.lock().text_info(specifier) } + pub fn update_config( + &self, + maybe_import_map: Option>, + maybe_config_file: Option<&ConfigFile>, + ) { + self + .0 + .lock() + .update_config(maybe_import_map, maybe_config_file) + } + /// Return the version of a document in the document cache. pub fn version(&self, specifier: &ModuleSpecifier) -> Option { self.0.lock().version(specifier) diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 89e286718c..73d028e766 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -116,7 +116,7 @@ pub(crate) struct Inner { /// file which will be used by the Deno LSP. maybe_config_uri: Option, /// An optional import map which is used to resolve modules. - pub(crate) maybe_import_map: Option, + pub(crate) maybe_import_map: Option>, /// The URL for the import map which is used to determine relative imports. maybe_import_map_uri: Option, /// A collection of measurements which instrument that performance of the LSP. @@ -481,13 +481,13 @@ impl Inner { ) })? }; - let import_map = - ImportMap::from_json(&import_map_url.to_string(), &import_map_json)?; + let import_map = Arc::new(ImportMap::from_json( + &import_map_url.to_string(), + &import_map_json, + )?); self.maybe_import_map_uri = Some(import_map_url); - self.maybe_import_map = Some(import_map.clone()); - self.documents.set_import_map(Some(Arc::new(import_map))); + self.maybe_import_map = Some(import_map); } else { - self.documents.set_import_map(None); self.maybe_import_map = None; } self.performance.measure(mark); @@ -700,6 +700,10 @@ impl Inner { if let Err(err) = self.update_registries().await { self.client.show_message(MessageType::Warning, err).await; } + self.documents.update_config( + self.maybe_import_map.clone(), + self.maybe_config_file.as_ref(), + ); self.performance.measure(mark); Ok(InitializeResult { @@ -908,6 +912,10 @@ impl Inner { if let Err(err) = self.diagnostics_server.update() { error!("{}", err); } + self.documents.update_config( + self.maybe_import_map.clone(), + self.maybe_config_file.as_ref(), + ); self.performance.measure(mark); } @@ -942,6 +950,10 @@ impl Inner { } } if touched { + self.documents.update_config( + self.maybe_import_map.clone(), + self.maybe_config_file.as_ref(), + ); self.diagnostics_server.invalidate_all().await; if let Err(err) = self.diagnostics_server.update() { error!("Cannot update diagnostics: {}", err); @@ -2624,6 +2636,7 @@ impl Inner { CacheServer::new( self.maybe_cache_path.clone(), self.maybe_import_map.clone(), + self.maybe_config_file.clone(), ) .await, ); diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs index 725ca07b30..27795e698c 100644 --- a/cli/lsp/mod.rs +++ b/cli/lsp/mod.rs @@ -19,7 +19,6 @@ mod path_to_regex; mod performance; mod refactor; mod registries; -mod resolver; mod semantic_tokens; mod text; mod tsc; diff --git a/cli/lsp/resolver.rs b/cli/lsp/resolver.rs deleted file mode 100644 index 4f768b697f..0000000000 --- a/cli/lsp/resolver.rs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. - -use deno_core::error::AnyError; -use deno_core::ModuleSpecifier; -use deno_graph::source::Resolver; -use import_map::ImportMap; -use std::sync::Arc; - -#[derive(Debug)] -pub(crate) struct ImportMapResolver(Arc); - -impl ImportMapResolver { - pub fn new(import_map: Arc) -> Self { - Self(import_map) - } - - pub fn as_resolver(&self) -> &dyn Resolver { - self - } -} - -impl Resolver for ImportMapResolver { - fn resolve( - &self, - specifier: &str, - referrer: &ModuleSpecifier, - ) -> Result { - self - .0 - .resolve(specifier, referrer.as_str()) - .map_err(|err| err.into()) - } -} diff --git a/cli/main.rs b/cli/main.rs index 7bf23556cf..da78488346 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -60,6 +60,7 @@ use crate::fmt_errors::PrettyJsError; use crate::module_loader::CliModuleLoader; use crate::proc_state::ProcState; use crate::resolver::ImportMapResolver; +use crate::resolver::JsxResolver; use crate::source_maps::apply_source_map; use crate::tools::installer::infer_name_from_url; use deno_ast::MediaType; @@ -468,14 +469,29 @@ async fn info_command( Permissions::allow_all(), ); let maybe_locker = lockfile::as_maybe_locker(ps.lockfile.clone()); - let maybe_resolver = - ps.maybe_import_map.as_ref().map(ImportMapResolver::new); + let maybe_import_map_resolver = + ps.maybe_import_map.clone().map(ImportMapResolver::new); + let maybe_jsx_resolver = ps + .maybe_config_file + .as_ref() + .map(|cf| { + cf.to_maybe_jsx_import_source_module() + .map(|im| JsxResolver::new(im, maybe_import_map_resolver.clone())) + }) + .flatten(); + let maybe_resolver = if maybe_jsx_resolver.is_some() { + maybe_jsx_resolver.as_ref().map(|jr| jr.as_resolver()) + } else { + maybe_import_map_resolver + .as_ref() + .map(|im| im.as_resolver()) + }; let graph = deno_graph::create_graph( vec![specifier], false, None, &mut cache, - maybe_resolver.as_ref().map(|r| r.as_resolver()), + maybe_resolver, maybe_locker, None, ) @@ -637,19 +653,35 @@ async fn create_graph_and_maybe_check( Permissions::allow_all(), ); let maybe_locker = lockfile::as_maybe_locker(ps.lockfile.clone()); - let maybe_imports = ps + let maybe_imports = if let Some(config_file) = &ps.maybe_config_file { + config_file.to_maybe_imports()? + } else { + None + }; + let maybe_import_map_resolver = + ps.maybe_import_map.clone().map(ImportMapResolver::new); + let maybe_jsx_resolver = ps .maybe_config_file .as_ref() - .map(|cf| cf.to_maybe_imports()) + .map(|cf| { + cf.to_maybe_jsx_import_source_module() + .map(|im| JsxResolver::new(im, maybe_import_map_resolver.clone())) + }) .flatten(); - let maybe_resolver = ps.maybe_import_map.as_ref().map(ImportMapResolver::new); + let maybe_resolver = if maybe_jsx_resolver.is_some() { + maybe_jsx_resolver.as_ref().map(|jr| jr.as_resolver()) + } else { + maybe_import_map_resolver + .as_ref() + .map(|im| im.as_resolver()) + }; let graph = Arc::new( deno_graph::create_graph( vec![root], false, maybe_imports, &mut cache, - maybe_resolver.as_ref().map(|r| r.as_resolver()), + maybe_resolver, maybe_locker, None, ) @@ -965,19 +997,34 @@ async fn run_with_watch(flags: Flags, script: String) -> Result<(), AnyError> { Permissions::allow_all(), ); let maybe_locker = lockfile::as_maybe_locker(ps.lockfile.clone()); - let maybe_imports = ps + let maybe_imports = if let Some(config_file) = &ps.maybe_config_file { + config_file.to_maybe_imports()? + } else { + None + }; + let maybe_import_map_resolver = + ps.maybe_import_map.clone().map(ImportMapResolver::new); + let maybe_jsx_resolver = ps .maybe_config_file .as_ref() - .map(|cf| cf.to_maybe_imports()) + .map(|cf| { + cf.to_maybe_jsx_import_source_module() + .map(|im| JsxResolver::new(im, maybe_import_map_resolver.clone())) + }) .flatten(); - let maybe_resolver = - ps.maybe_import_map.as_ref().map(ImportMapResolver::new); + let maybe_resolver = if maybe_jsx_resolver.is_some() { + maybe_jsx_resolver.as_ref().map(|jr| jr.as_resolver()) + } else { + maybe_import_map_resolver + .as_ref() + .map(|im| im.as_resolver()) + }; let graph = deno_graph::create_graph( vec![main_module.clone()], false, maybe_imports, &mut cache, - maybe_resolver.as_ref().map(|r| r.as_resolver()), + maybe_resolver, maybe_locker, None, ) diff --git a/cli/ops/runtime_compiler.rs b/cli/ops/runtime_compiler.rs index 8f7a75146d..b16fbcf694 100644 --- a/cli/ops/runtime_compiler.rs +++ b/cli/ops/runtime_compiler.rs @@ -7,6 +7,7 @@ use crate::emit; use crate::errors::get_error_class_name; use crate::proc_state::ProcState; use crate::resolver::ImportMapResolver; +use crate::resolver::JsxResolver; use deno_core::error::custom_error; use deno_core::error::generic_error; @@ -71,14 +72,66 @@ struct EmitResult { stats: emit::Stats, } +/// Provides inferred imported modules from configuration options, like the +/// `"types"` and `"jsxImportSource"` imports. fn to_maybe_imports( referrer: &ModuleSpecifier, maybe_options: Option<&HashMap>, ) -> Option)>> { - let options = maybe_options.as_ref()?; - let types_value = options.get("types")?; - let types: Vec = serde_json::from_value(types_value.clone()).ok()?; - Some(vec![(referrer.clone(), types)]) + let options = maybe_options?; + let mut imports = Vec::new(); + if let Some(types_value) = options.get("types") { + if let Ok(types) = + serde_json::from_value::>(types_value.clone()) + { + imports.extend(types); + } + } + if let Some(jsx_value) = options.get("jsx") { + if let Ok(jsx) = serde_json::from_value::(jsx_value.clone()) { + let jsx_import_source = + if let Some(jsx_import_source_value) = options.get("jsxImportSource") { + if let Ok(jsx_import_source) = + serde_json::from_value::(jsx_import_source_value.clone()) + { + jsx_import_source + } else { + "react".to_string() + } + } else { + "react".to_string() + }; + match jsx.as_str() { + "react-jsx" => { + imports.push(format!("{}/jsx-runtime", jsx_import_source)); + } + "react-jsxdev" => { + imports.push(format!("{}/jsx-dev-runtime", jsx_import_source)); + } + _ => (), + } + } + } + if !imports.is_empty() { + Some(vec![(referrer.clone(), imports)]) + } else { + None + } +} + +/// Converts the compiler options to the JSX import source module that will be +/// loaded when transpiling JSX. +fn to_maybe_jsx_import_source_module( + maybe_options: Option<&HashMap>, +) -> Option { + let options = maybe_options?; + let jsx_value = options.get("jsx")?; + let jsx: String = serde_json::from_value(jsx_value.clone()).ok()?; + match jsx.as_str() { + "react-jsx" => Some("jsx-runtime".to_string()), + "react-jsxdev" => Some("jsx-dev-runtime".to_string()), + _ => None, + } } async fn op_emit( @@ -108,7 +161,9 @@ async fn op_emit( runtime_permissions.clone(), )) }; - let maybe_import_map = if let Some(import_map_str) = args.import_map_path { + let maybe_import_map_resolver = if let Some(import_map_str) = + args.import_map_path + { let import_map_specifier = resolve_url_or_path(&import_map_str) .context(format!("Bad URL (\"{}\") for import map.", import_map_str))?; let import_map = if let Some(value) = args.import_map { @@ -126,23 +181,32 @@ async fn op_emit( })?; ImportMap::from_json(import_map_specifier.as_str(), &file.source)? }; - Some(import_map) + Some(ImportMapResolver::new(Arc::new(import_map))) } else if args.import_map.is_some() { return Err(generic_error("An importMap was specified, but no importMapPath was provided, which is required.")); } else { None }; + let maybe_jsx_resolver = + to_maybe_jsx_import_source_module(args.compiler_options.as_ref()) + .map(|im| JsxResolver::new(im, maybe_import_map_resolver.clone())); + let maybe_resolver = if maybe_jsx_resolver.is_some() { + maybe_jsx_resolver.as_ref().map(|jr| jr.as_resolver()) + } else { + maybe_import_map_resolver + .as_ref() + .map(|imr| imr.as_resolver()) + }; let roots = vec![resolve_url_or_path(&root_specifier)?]; let maybe_imports = to_maybe_imports(&roots[0], args.compiler_options.as_ref()); - let maybe_resolver = maybe_import_map.as_ref().map(ImportMapResolver::new); let graph = Arc::new( deno_graph::create_graph( roots, true, maybe_imports, cache.as_mut_loader(), - maybe_resolver.as_ref().map(|r| r.as_resolver()), + maybe_resolver, None, None, ) diff --git a/cli/proc_state.rs b/cli/proc_state.rs index 0c81ab2e88..ddfd430347 100644 --- a/cli/proc_state.rs +++ b/cli/proc_state.rs @@ -5,6 +5,7 @@ use crate::colors; use crate::compat; use crate::compat::NodeEsmResolver; use crate::config_file::ConfigFile; +use crate::config_file::MaybeImportsResult; use crate::deno_dir; use crate::emit; use crate::errors::get_module_graph_error_class; @@ -15,6 +16,7 @@ use crate::http_cache; use crate::lockfile::as_maybe_locker; use crate::lockfile::Lockfile; use crate::resolver::ImportMapResolver; +use crate::resolver::JsxResolver; use crate::source_maps::SourceMapGetter; use crate::version; @@ -79,7 +81,7 @@ pub struct Inner { graph_data: Arc>, pub lockfile: Option>>, pub maybe_config_file: Option, - pub maybe_import_map: Option, + pub maybe_import_map: Option>, pub maybe_inspector_server: Option>, pub root_cert_store: Option, pub blob_store: BlobStore, @@ -200,7 +202,7 @@ impl ProcState { None }; - let maybe_import_map: Option = + let maybe_import_map: Option> = match flags.import_map_path.as_ref() { None => None, Some(import_map_url) => { @@ -218,7 +220,7 @@ impl ProcState { ))?; let import_map = ImportMap::from_json(import_map_specifier.as_str(), &file.source)?; - Some(import_map) + Some(Arc::new(import_map)) } }; @@ -258,10 +260,10 @@ impl ProcState { /// Return any imports that should be brought into the scope of the module /// graph. - fn get_maybe_imports(&self) -> Option)>> { + fn get_maybe_imports(&self) -> MaybeImportsResult { let mut imports = Vec::new(); if let Some(config_file) = &self.maybe_config_file { - if let Some(config_imports) = config_file.to_maybe_imports() { + if let Some(config_imports) = config_file.to_maybe_imports()? { imports.extend(config_imports); } } @@ -269,9 +271,9 @@ impl ProcState { imports.extend(compat::get_node_imports()); } if imports.is_empty() { - None + Ok(None) } else { - Some(imports) + Ok(Some(imports)) } } @@ -297,16 +299,30 @@ impl ProcState { dynamic_permissions.clone(), ); let maybe_locker = as_maybe_locker(self.lockfile.clone()); - let maybe_imports = self.get_maybe_imports(); + let maybe_imports = self.get_maybe_imports()?; let node_resolver = NodeEsmResolver::new( - self.maybe_import_map.as_ref().map(ImportMapResolver::new), + self.maybe_import_map.clone().map(ImportMapResolver::new), ); - let import_map_resolver = - self.maybe_import_map.as_ref().map(ImportMapResolver::new); + let maybe_import_map_resolver = + self.maybe_import_map.clone().map(ImportMapResolver::new); + let maybe_jsx_resolver = self + .maybe_config_file + .as_ref() + .map(|cf| { + cf.to_maybe_jsx_import_source_module() + .map(|im| JsxResolver::new(im, maybe_import_map_resolver.clone())) + }) + .flatten(); let maybe_resolver = if self.flags.compat { Some(node_resolver.as_resolver()) + } else if maybe_jsx_resolver.is_some() { + // the JSX resolver offloads to the import map if present, otherwise uses + // the default Deno explicit import resolution. + maybe_jsx_resolver.as_ref().map(|jr| jr.as_resolver()) } else { - import_map_resolver.as_ref().map(|im| im.as_resolver()) + maybe_import_map_resolver + .as_ref() + .map(|im| im.as_resolver()) }; // TODO(bartlomieju): this is very make-shift, is there an existing API // that we could include it like with "maybe_imports"? diff --git a/cli/resolver.rs b/cli/resolver.rs index d3427c58b5..da8fafe672 100644 --- a/cli/resolver.rs +++ b/cli/resolver.rs @@ -1,27 +1,29 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. use deno_core::error::AnyError; +use deno_core::resolve_import; use deno_core::ModuleSpecifier; use deno_graph::source::Resolver; use import_map::ImportMap; +use std::sync::Arc; /// Wraps an import map to be used when building a deno_graph module graph. /// This is done to avoid having `import_map` be a direct dependency of /// `deno_graph`. -#[derive(Debug)] -pub(crate) struct ImportMapResolver<'a>(&'a ImportMap); +#[derive(Debug, Clone)] +pub(crate) struct ImportMapResolver(Arc); -impl<'a> ImportMapResolver<'a> { - pub fn new(import_map: &'a ImportMap) -> Self { +impl ImportMapResolver { + pub fn new(import_map: Arc) -> Self { Self(import_map) } - pub fn as_resolver(&'a self) -> &'a dyn Resolver { + pub fn as_resolver(&self) -> &dyn Resolver { self } } -impl Resolver for ImportMapResolver<'_> { +impl Resolver for ImportMapResolver { fn resolve( &self, specifier: &str, @@ -33,3 +35,42 @@ impl Resolver for ImportMapResolver<'_> { .map_err(|err| err.into()) } } + +#[derive(Debug, Default, Clone)] +pub(crate) struct JsxResolver { + jsx_import_source_module: String, + maybe_import_map_resolver: Option, +} + +impl JsxResolver { + pub fn new( + jsx_import_source_module: String, + maybe_import_map_resolver: Option, + ) -> Self { + Self { + jsx_import_source_module, + maybe_import_map_resolver, + } + } + + pub fn as_resolver(&self) -> &dyn Resolver { + self + } +} + +impl Resolver for JsxResolver { + fn jsx_import_source_module(&self) -> &str { + self.jsx_import_source_module.as_str() + } + + fn resolve( + &self, + specifier: &str, + referrer: &ModuleSpecifier, + ) -> Result { + self.maybe_import_map_resolver.as_ref().map_or_else( + || resolve_import(specifier, referrer.as_str()).map_err(|err| err.into()), + |r| r.resolve(specifier, referrer), + ) + } +} diff --git a/cli/schemas/config-file.v1.json b/cli/schemas/config-file.v1.json index 8f9866f174..b8c4622a15 100644 --- a/cli/schemas/config-file.v1.json +++ b/cli/schemas/config-file.v1.json @@ -63,6 +63,12 @@ "default": "React.Fragment", "markdownDescription": "Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'.\n\nSee more: https://www.typescriptlang.org/tsconfig#jsxFragmentFactory" }, + "jsxImportSource": { + "description": "Specify module specifier used to import the JSX factory functions when using jsx: 'react-jsx*'.", + "type": "string", + "default": "react", + "markdownDescription": "Specify module specifier used to import the JSX factory functions when using jsx: `react-jsx*`.\n\nSee more: https://www.typescriptlang.org/tsconfig/#jsxImportSource" + }, "keyofStringsOnly": { "description": "Make keyof only return strings instead of string, numbers or symbols. Legacy option.", "type": "boolean", @@ -73,7 +79,9 @@ "description": "Specify a set of bundled library declaration files that describe the target runtime environment.", "type": "array", "uniqueItems": true, - "default": ["deno.window"], + "default": [ + "deno.window" + ], "items": { "type": "string" }, diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs index 6e009bd20e..fc4f6dbc6e 100644 --- a/cli/tests/integration/lsp_tests.rs +++ b/cli/tests/integration/lsp_tests.rs @@ -3684,3 +3684,82 @@ fn lsp_lint_with_config() { } shutdown(&mut client); } + +#[test] +fn lsp_jsx_import_source_pragma() { + let _g = http_server(); + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.tsx", + "languageId": "typescriptreact", + "version": 1, + "text": +"/** @jsxImportSource http://localhost:4545/jsx */ + +function A() { + return \"hello\"; +} + +export function B() { + return ; +} +", + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "deno/cache", + json!({ + "referrer": { + "uri": "file:///a/file.tsx", + }, + "uris": [ + { + "uri": "http://127.0.0.1:4545/jsx/jsx-runtime", + } + ], + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.tsx" + }, + "position": { + "line": 0, + "character": 25 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: http​://localhost:4545/jsx/jsx-runtime\n", + }, + "range": { + "start": { + "line": 0, + "character": 21 + }, + "end": { + "line": 0, + "character": 46 + } + } + })) + ); + shutdown(&mut client); +} diff --git a/cli/tests/integration/mod.rs b/cli/tests/integration/mod.rs index 251fca5150..21ffc56279 100644 --- a/cli/tests/integration/mod.rs +++ b/cli/tests/integration/mod.rs @@ -1100,7 +1100,7 @@ fn basic_auth_tokens() { eprintln!("{}", stderr_str); assert!(stderr_str.contains( - "Import 'http://127.0.0.1:4554/001_hello.js' failed: 404 Not Found" + "Import 'http://127.0.0.1:4554/001_hello.js' failed, not found." )); let output = util::deno_cmd() diff --git a/cli/tests/integration/run_tests.rs b/cli/tests/integration/run_tests.rs index f290c6e627..214eb8ecee 100644 --- a/cli/tests/integration/run_tests.rs +++ b/cli/tests/integration/run_tests.rs @@ -1215,6 +1215,118 @@ itest!(jsx_import_from_ts { output: "jsx_import_from_ts.ts.out", }); +itest!(jsx_import_source_pragma { + args: "run --reload jsx_import_source_pragma.tsx", + output: "jsx_import_source.out", + http_server: true, +}); + +itest!(jsx_import_source_pragma_with_config { + args: "run --reload --config jsx/deno-jsx.jsonc jsx_import_source_pragma.tsx", + output: "jsx_import_source.out", + http_server: true, +}); + +itest!(jsx_import_source_pragma_with_dev_config { + args: + "run --reload --config jsx/deno-jsxdev.jsonc jsx_import_source_pragma.tsx", + output: "jsx_import_source_dev.out", + http_server: true, +}); + +itest!(jsx_import_source_no_pragma { + args: + "run --reload --config jsx/deno-jsx.jsonc jsx_import_source_no_pragma.tsx", + output: "jsx_import_source.out", + http_server: true, +}); + +itest!(jsx_import_source_no_pragma_dev { + args: "run --reload --config jsx/deno-jsxdev.jsonc jsx_import_source_no_pragma.tsx", + output: "jsx_import_source_dev.out", + http_server: true, +}); + +itest!(jsx_import_source_pragma_import_map { + args: "run --reload --import-map jsx/import-map.json jsx_import_source_pragma_import_map.tsx", + output: "jsx_import_source_import_map.out", + http_server: true, +}); + +itest!(jsx_import_source_pragma_import_map_dev { + args: "run --reload --import-map jsx/import-map.json --config jsx/deno-jsxdev-import-map.jsonc jsx_import_source_pragma_import_map.tsx", + output: "jsx_import_source_import_map_dev.out", + http_server: true, +}); + +itest!(jsx_import_source_import_map { + args: "run --reload --import-map jsx/import-map.json --config jsx/deno-jsx-import-map.jsonc jsx_import_source_no_pragma.tsx", + output: "jsx_import_source_import_map.out", + http_server: true, +}); + +itest!(jsx_import_source_import_map_dev { + args: "run --reload --import-map jsx/import-map.json --config jsx/deno-jsxdev-import-map.jsonc jsx_import_source_no_pragma.tsx", + output: "jsx_import_source_import_map_dev.out", + http_server: true, +}); + +itest!(jsx_import_source_pragma_no_check { + args: "run --reload --no-check jsx_import_source_pragma.tsx", + output: "jsx_import_source.out", + http_server: true, +}); + +itest!(jsx_import_source_pragma_with_config_no_check { + args: "run --reload --config jsx/deno-jsx.jsonc --no-check jsx_import_source_pragma.tsx", + output: "jsx_import_source.out", + http_server: true, +}); + +// itest!(jsx_import_source_pragma_with_dev_config_no_check { +// args: +// "run --reload --config jsx/deno-jsxdev.jsonc --no-check jsx_import_source_pragma.tsx", +// output: "jsx_import_source_dev.out", +// http_server: true, +// }); + +itest!(jsx_import_source_no_pragma_no_check { + args: + "run --reload --config jsx/deno-jsx.jsonc --no-check jsx_import_source_no_pragma.tsx", + output: "jsx_import_source.out", + http_server: true, +}); + +// itest!(jsx_import_source_no_pragma_dev_no_check { +// args: "run --reload --config jsx/deno-jsxdev.jsonc --no-check jsx_import_source_no_pragma.tsx", +// output: "jsx_import_source_dev.out", +// http_server: true, +// }); + +itest!(jsx_import_source_pragma_import_map_no_check { + args: "run --reload --import-map jsx/import-map.json --no-check jsx_import_source_pragma_import_map.tsx", + output: "jsx_import_source_import_map.out", + http_server: true, +}); + +// itest!(jsx_import_source_pragma_import_map_dev_no_check { +// args: "run --reload --import-map jsx/import-map.json --config jsx/deno-jsxdev-import-map.jsonc --no-check jsx_import_source_pragma_import_map.tsx", +// output: "jsx_import_source_import_map_dev.out", +// http_server: true, +// }); + +itest!(jsx_import_source_import_map_no_check { + args: "run --reload --import-map jsx/import-map.json --config jsx/deno-jsx-import-map.jsonc --no-check jsx_import_source_no_pragma.tsx", + output: "jsx_import_source_import_map.out", + http_server: true, +}); + +// itest!(jsx_import_source_import_map_dev_no_check { +// args: "run --reload --import-map jsx/import-map.json --config jsx/deno-jsxdev-import-map.jsonc --no-check jsx_import_source_no_pragma.tsx", +// output: "jsx_import_source_import_map_dev.out", +// http_server: true, +// }); + // TODO(#11128): Flaky. Re-enable later. // itest!(single_compile_with_reload { // args: "run --reload --allow-read single_compile_with_reload.ts", diff --git a/cli/tests/testdata/compiler_api_test.ts b/cli/tests/testdata/compiler_api_test.ts index 9870908d16..42d6f54ebf 100644 --- a/cli/tests/testdata/compiler_api_test.ts +++ b/cli/tests/testdata/compiler_api_test.ts @@ -557,3 +557,81 @@ Deno.test({ assertEquals(sourceMap.sourcesContent.length, 1); }, }); + +Deno.test({ + name: "Deno.emit() - JSX import source pragma", + async fn() { + const { files } = await Deno.emit( + "file:///a.tsx", + { + sources: { + "file:///a.tsx": `/** @jsxImportSource https://example.com/jsx */ + + export function App() { + return ( +
<>
+ ); + }`, + "https://example.com/jsx/jsx-runtime": `export function jsx( + _type, + _props, + _key, + _source, + _self, + ) {} + export const jsxs = jsx; + export const jsxDEV = jsx; + export const Fragment = Symbol("Fragment"); + console.log("imported", import.meta.url); + `, + }, + }, + ); + assert(files["file:///a.tsx.js"]); + assert( + files["file:///a.tsx.js"].startsWith( + `import { Fragment as _Fragment, jsx as _jsx } from "https://example.com/jsx/jsx-runtime";\n`, + ), + ); + }, +}); + +Deno.test({ + name: "Deno.emit() - JSX import source no pragma", + async fn() { + const { files } = await Deno.emit( + "file:///a.tsx", + { + compilerOptions: { + jsx: "react-jsx", + jsxImportSource: "https://example.com/jsx", + }, + sources: { + "file:///a.tsx": `export function App() { + return ( +
<>
+ ); + }`, + "https://example.com/jsx/jsx-runtime": `export function jsx( + _type, + _props, + _key, + _source, + _self, + ) {} + export const jsxs = jsx; + export const jsxDEV = jsx; + export const Fragment = Symbol("Fragment"); + console.log("imported", import.meta.url); + `, + }, + }, + ); + assert(files["file:///a.tsx.js"]); + assert( + files["file:///a.tsx.js"].startsWith( + `import { Fragment as _Fragment, jsx as _jsx } from "https://example.com/jsx/jsx-runtime";\n`, + ), + ); + }, +}); diff --git a/cli/tests/testdata/jsx/deno-jsx-import-map.jsonc b/cli/tests/testdata/jsx/deno-jsx-import-map.jsonc new file mode 100644 index 0000000000..5adbfa8b5a --- /dev/null +++ b/cli/tests/testdata/jsx/deno-jsx-import-map.jsonc @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "jsx" + } +} diff --git a/cli/tests/testdata/jsx/deno-jsx.jsonc b/cli/tests/testdata/jsx/deno-jsx.jsonc new file mode 100644 index 0000000000..311409ea37 --- /dev/null +++ b/cli/tests/testdata/jsx/deno-jsx.jsonc @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "http://localhost:4545/jsx" + } +} diff --git a/cli/tests/testdata/jsx/deno-jsxdev-import-map.jsonc b/cli/tests/testdata/jsx/deno-jsxdev-import-map.jsonc new file mode 100644 index 0000000000..7481d5a2d8 --- /dev/null +++ b/cli/tests/testdata/jsx/deno-jsxdev-import-map.jsonc @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "jsx": "react-jsxdev", + "jsxImportSource": "jsx" + } +} diff --git a/cli/tests/testdata/jsx/deno-jsxdev.jsonc b/cli/tests/testdata/jsx/deno-jsxdev.jsonc new file mode 100644 index 0000000000..ae5bdf9f16 --- /dev/null +++ b/cli/tests/testdata/jsx/deno-jsxdev.jsonc @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "jsx": "react-jsxdev", + "jsxImportSource": "http://localhost:4545/jsx" + } +} diff --git a/cli/tests/testdata/jsx/import-map.json b/cli/tests/testdata/jsx/import-map.json new file mode 100644 index 0000000000..baab76f209 --- /dev/null +++ b/cli/tests/testdata/jsx/import-map.json @@ -0,0 +1,6 @@ +{ + "imports": { + "jsx/jsx-runtime": "http://localhost:4545/jsx/jsx-runtime/index.ts", + "jsx/jsx-dev-runtime": "http://localhost:4545/jsx/jsx-dev-runtime/index.ts" + } +} diff --git a/cli/tests/testdata/jsx/jsx-dev-runtime/index.ts b/cli/tests/testdata/jsx/jsx-dev-runtime/index.ts new file mode 100644 index 0000000000..15e2029c8a --- /dev/null +++ b/cli/tests/testdata/jsx/jsx-dev-runtime/index.ts @@ -0,0 +1,12 @@ +// deno-lint-ignore-file no-explicit-any +export function jsx( + _type: any, + _props: any, + _key: any, + _source: any, + _self: any, +) {} +export const jsxs = jsx; +export const jsxDEV = jsx; +export const Fragment = Symbol("Fragment"); +console.log("imported", import.meta.url); diff --git a/cli/tests/testdata/jsx/jsx-runtime/index.ts b/cli/tests/testdata/jsx/jsx-runtime/index.ts new file mode 100644 index 0000000000..15e2029c8a --- /dev/null +++ b/cli/tests/testdata/jsx/jsx-runtime/index.ts @@ -0,0 +1,12 @@ +// deno-lint-ignore-file no-explicit-any +export function jsx( + _type: any, + _props: any, + _key: any, + _source: any, + _self: any, +) {} +export const jsxs = jsx; +export const jsxDEV = jsx; +export const Fragment = Symbol("Fragment"); +console.log("imported", import.meta.url); diff --git a/cli/tests/testdata/jsx_import_source.out b/cli/tests/testdata/jsx_import_source.out new file mode 100644 index 0000000000..b9555987a6 --- /dev/null +++ b/cli/tests/testdata/jsx_import_source.out @@ -0,0 +1,2 @@ +[WILDCARD] +imported http://localhost:4545/jsx/jsx-runtime diff --git a/cli/tests/testdata/jsx_import_source_dev.out b/cli/tests/testdata/jsx_import_source_dev.out new file mode 100644 index 0000000000..38d7a12f05 --- /dev/null +++ b/cli/tests/testdata/jsx_import_source_dev.out @@ -0,0 +1,2 @@ +[WILDCARD] +imported http://localhost:4545/jsx/jsx-dev-runtime diff --git a/cli/tests/testdata/jsx_import_source_import_map.out b/cli/tests/testdata/jsx_import_source_import_map.out new file mode 100644 index 0000000000..0d32389677 --- /dev/null +++ b/cli/tests/testdata/jsx_import_source_import_map.out @@ -0,0 +1,2 @@ +[WILDCARD] +imported http://localhost:4545/jsx/jsx-runtime/index.ts diff --git a/cli/tests/testdata/jsx_import_source_import_map_dev.out b/cli/tests/testdata/jsx_import_source_import_map_dev.out new file mode 100644 index 0000000000..56f514d90c --- /dev/null +++ b/cli/tests/testdata/jsx_import_source_import_map_dev.out @@ -0,0 +1,2 @@ +[WILDCARD] +imported http://localhost:4545/jsx/jsx-dev-runtime/index.ts diff --git a/cli/tests/testdata/jsx_import_source_no_pragma.tsx b/cli/tests/testdata/jsx_import_source_no_pragma.tsx new file mode 100644 index 0000000000..2c756054fb --- /dev/null +++ b/cli/tests/testdata/jsx_import_source_no_pragma.tsx @@ -0,0 +1,7 @@ +function A() { + return "hello"; +} + +export function B() { + return ; +} diff --git a/cli/tests/testdata/jsx_import_source_pragma.tsx b/cli/tests/testdata/jsx_import_source_pragma.tsx new file mode 100644 index 0000000000..c19e53d4ff --- /dev/null +++ b/cli/tests/testdata/jsx_import_source_pragma.tsx @@ -0,0 +1,9 @@ +/** @jsxImportSource http://localhost:4545/jsx */ + +function A() { + return "hello"; +} + +export function B() { + return ; +} diff --git a/cli/tests/testdata/jsx_import_source_pragma_import_map.tsx b/cli/tests/testdata/jsx_import_source_pragma_import_map.tsx new file mode 100644 index 0000000000..548365f182 --- /dev/null +++ b/cli/tests/testdata/jsx_import_source_pragma_import_map.tsx @@ -0,0 +1,9 @@ +/** @jsxImportSource jsx */ + +function A() { + return "hello"; +} + +export function B() { + return ; +} diff --git a/cli/tools/doc.rs b/cli/tools/doc.rs index aa9d913a01..cc37df06d2 100644 --- a/cli/tools/doc.rs +++ b/cli/tools/doc.rs @@ -38,7 +38,7 @@ impl Loader for StubDocLoader { #[derive(Debug)] struct DocResolver { - import_map: Option, + import_map: Option>, } impl Resolver for DocResolver { diff --git a/cli/tools/repl.rs b/cli/tools/repl.rs index f3ba626af9..b6874f574d 100644 --- a/cli/tools/repl.rs +++ b/cli/tools/repl.rs @@ -668,8 +668,11 @@ impl ReplSession { imports_not_used_as_values: ImportsNotUsedAsValues::Preserve, // JSX is not supported in the REPL transform_jsx: false, + jsx_automatic: false, + jsx_development: false, jsx_factory: "React.createElement".into(), jsx_fragment_factory: "React.Fragment".into(), + jsx_import_source: None, repl_imports: true, }, )? diff --git a/cli/tools/test.rs b/cli/tools/test.rs index d883f18a34..fba1782024 100644 --- a/cli/tools/test.rs +++ b/cli/tools/test.rs @@ -18,6 +18,7 @@ use crate::lockfile; use crate::ops; use crate::proc_state::ProcState; use crate::resolver::ImportMapResolver; +use crate::resolver::JsxResolver; use crate::tools::coverage::CoverageCollector; use deno_ast::swc::common::comments::CommentKind; @@ -1053,14 +1054,21 @@ pub async fn run_tests_with_watch( let paths_to_watch = paths_to_watch.clone(); let paths_to_watch_clone = paths_to_watch.clone(); - let maybe_resolver = - ps.maybe_import_map.as_ref().map(ImportMapResolver::new); + let maybe_import_map_resolver = + ps.maybe_import_map.clone().map(ImportMapResolver::new); + let maybe_jsx_resolver = ps + .maybe_config_file + .as_ref() + .map(|cf| { + cf.to_maybe_jsx_import_source_module() + .map(|im| JsxResolver::new(im, maybe_import_map_resolver.clone())) + }) + .flatten(); let maybe_locker = lockfile::as_maybe_locker(ps.lockfile.clone()); let maybe_imports = ps .maybe_config_file .as_ref() - .map(|cf| cf.to_maybe_imports()) - .flatten(); + .map(|cf| cf.to_maybe_imports()); let files_changed = changed.is_some(); let include = include.clone(); let ignore = ignore.clone(); @@ -1081,13 +1089,24 @@ pub async fn run_tests_with_watch( .filter_map(|url| deno_core::resolve_url(url.as_str()).ok()) .collect() }; - + let maybe_imports = if let Some(result) = maybe_imports { + result? + } else { + None + }; + let maybe_resolver = if maybe_jsx_resolver.is_some() { + maybe_jsx_resolver.as_ref().map(|jr| jr.as_resolver()) + } else { + maybe_import_map_resolver + .as_ref() + .map(|im| im.as_resolver()) + }; let graph = deno_graph::create_graph( test_modules.clone(), false, maybe_imports, cache.as_mut_loader(), - maybe_resolver.as_ref().map(|r| r.as_resolver()), + maybe_resolver, maybe_locker, None, ) diff --git a/cli/tsc.rs b/cli/tsc.rs index bb377c5d84..5f5c09539c 100644 --- a/cli/tsc.rs +++ b/cli/tsc.rs @@ -414,6 +414,27 @@ pub struct ResolveArgs { pub specifiers: Vec, } +fn resolve_specifier( + state: &mut State, + specifier: &ModuleSpecifier, +) -> (String, String) { + let media_type = state + .graph + .get(specifier) + .map_or(&MediaType::Unknown, |m| &m.media_type); + let specifier_str = match specifier.scheme() { + "data" | "blob" => { + let specifier_str = hash_url(specifier, media_type); + state + .data_url_map + .insert(specifier_str.clone(), specifier.clone()); + specifier_str + } + _ => specifier.to_string(), + }; + (specifier_str, media_type.as_ts_extension().into()) +} + fn op_resolve(state: &mut State, args: Value) -> Result { let v: ResolveArgs = serde_json::from_value(args) .context("Invalid request from JavaScript for \"op_resolve\".")?; @@ -434,30 +455,31 @@ fn op_resolve(state: &mut State, args: Value) -> Result { MediaType::from(specifier).as_ts_extension().to_string(), )); } else { - let resolved_dependency = - match state.graph.resolve_dependency(specifier, &referrer, true) { - Some(resolved_specifier) => { - let media_type = state - .graph - .get(resolved_specifier) - .map_or(&MediaType::Unknown, |m| &m.media_type); - let resolved_specifier_str = match resolved_specifier.scheme() { - "data" | "blob" => { - let specifier_str = hash_url(resolved_specifier, media_type); - state - .data_url_map - .insert(specifier_str.clone(), resolved_specifier.clone()); - specifier_str - } - _ => resolved_specifier.to_string(), - }; - (resolved_specifier_str, media_type.as_ts_extension().into()) - } - None => ( - "deno:///missing_dependency.d.ts".to_string(), - ".d.ts".to_string(), - ), - }; + // here, we try to resolve the specifier via the referrer, but if we can't + // we will try to resolve the specifier via the configuration file, if + // present, finally defaulting to a "placeholder" specifier. This handles + // situations like the jsxImportSource, which tsc tries to resolve the + // import source from a JSX module, but the module graph only contains the + // import as a dependency of the configuration file. + let resolved_dependency = if let Some(resolved_specifier) = state + .graph + .resolve_dependency(specifier, &referrer, true) + .cloned() + { + resolve_specifier(state, &resolved_specifier) + } else if let Some(resolved_specifier) = state + .maybe_config_specifier + .as_ref() + .map(|cf| state.graph.resolve_dependency(specifier, cf, true).cloned()) + .flatten() + { + resolve_specifier(state, &resolved_specifier) + } else { + ( + "deno:///missing_dependency.d.ts".to_string(), + ".d.ts".to_string(), + ) + }; resolved.push(resolved_dependency); } } diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs index 52924ac909..e57f55f1ee 100644 --- a/test_util/src/lib.rs +++ b/test_util/src/lib.rs @@ -566,7 +566,9 @@ async fn absolute_redirect( Ok(file_resp) } -async fn main_server(req: Request) -> hyper::Result> { +async fn main_server( + req: Request, +) -> Result, hyper::http::Error> { return match (req.method(), req.uri().path()) { (&hyper::Method::POST, "/echo_server") => { let (parts, body) = req.into_parts(); @@ -849,6 +851,27 @@ async fn main_server(req: Request) -> hyper::Result> { let version = format!("{:?}", req.version()); Ok(Response::new(version.into())) } + (_, "/jsx/jsx-runtime") | (_, "/jsx/jsx-dev-runtime") => { + let mut res = Response::new(Body::from( + r#"export function jsx( + _type, + _props, + _key, + _source, + _self, + ) {} + export const jsxs = jsx; + export const jsxDEV = jsx; + export const Fragment = Symbol("Fragment"); + console.log("imported", import.meta.url); + "#, + )); + res.headers_mut().insert( + "Content-type", + HeaderValue::from_static("application/javascript"), + ); + Ok(res) + } _ => { let mut file_path = testdata_path(); file_path.push(&req.uri().path()[1..]); @@ -857,7 +880,9 @@ async fn main_server(req: Request) -> hyper::Result> { return Ok(file_resp); } - return Ok(Response::new(Body::empty())); + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty()) } }; }