feat(cli): support React 17 JSX transforms (#12631)

Closes #8440
This commit is contained in:
Kitson Kelly 2021-11-09 12:26:39 +11:00 committed by GitHub
parent 45425c1146
commit f5eb177f50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1115 additions and 173 deletions

8
Cargo.lock generated
View file

@ -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",

View file

@ -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" }

View file

@ -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<String>,
/// 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<config_file::TsConfig> 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 (
<div><></></div>
);
}"#;
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 (
<div><></></div>
);
}"#;
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 (
// <div><></></div>
// );
// }"#;
// 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")

View file

@ -14,12 +14,12 @@ use regex::Regex;
use std::path::PathBuf;
#[derive(Debug, Default)]
pub(crate) struct NodeEsmResolver<'a> {
maybe_import_map_resolver: Option<ImportMapResolver<'a>>,
pub(crate) struct NodeEsmResolver {
maybe_import_map_resolver: Option<ImportMapResolver>,
}
impl<'a> NodeEsmResolver<'a> {
pub fn new(maybe_import_map_resolver: Option<ImportMapResolver<'a>>) -> Self {
impl NodeEsmResolver {
pub fn new(maybe_import_map_resolver: Option<ImportMapResolver>) -> 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,

View file

@ -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<Option<Vec<(ModuleSpecifier, Vec<String>)>>, 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<String>,
}
/// 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<String>,
pub jsx_import_source: Option<String>,
pub types: Option<Vec<String>>,
}
@ -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<Vec<(ModuleSpecifier, Vec<String>)>> {
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<String> {
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<Option<FmtConfig>, AnyError> {

View file

@ -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;

View file

@ -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);
}

View file

@ -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<Request>);
impl CacheServer {
pub async fn new(
maybe_cache_path: Option<PathBuf>,
maybe_import_map: Option<ImportMap>,
maybe_import_map: Option<Arc<ImportMap>>,
maybe_config_file: Option<ConfigFile>,
) -> Self {
let (tx, mut rx) = mpsc::unbounded_channel::<Request>();
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,
)

View file

@ -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<String, deno_graph::Resolved>,
specifier: ModuleSpecifier,
}
impl SyntheticModule {
pub fn new(
specifier: ModuleSpecifier,
dependencies: Vec<(String, Option<lsp::Range>)>,
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::SpecifierError>()
{
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<LineIndex>,
@ -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<ModuleSpecifier, Document>,
/// Any imports to the context supplied by configuration files. This is like
/// the imports into the a module graph in CLI.
imports: HashMap<ModuleSpecifier, SyntheticModule>,
/// The optional import map that should be used when resolving dependencies.
maybe_import_map: Option<ImportMapResolver>,
/// The optional JSX resolver, which is used when JSX imports are configured.
maybe_jsx_resolver: Option<JsxResolver>,
redirects: HashMap<ModuleSpecifier, ModuleSpecifier>,
}
@ -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<lsp::TextDocumentContentChangeEvent>,
) -> 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<String>,
) {
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<Arc<import_map::ImportMap>>,
) {
// 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<Arc<import_map::ImportMap>>,
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<String> {
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<Arc<import_map::ImportMap>>,
) {
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<Arc<import_map::ImportMap>>,
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<String> {
self.0.lock().version(specifier)

View file

@ -116,7 +116,7 @@ pub(crate) struct Inner {
/// file which will be used by the Deno LSP.
maybe_config_uri: Option<Url>,
/// An optional import map which is used to resolve modules.
pub(crate) maybe_import_map: Option<ImportMap>,
pub(crate) maybe_import_map: Option<Arc<ImportMap>>,
/// The URL for the import map which is used to determine relative imports.
maybe_import_map_uri: Option<Url>,
/// 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,
);

View file

@ -19,7 +19,6 @@ mod path_to_regex;
mod performance;
mod refactor;
mod registries;
mod resolver;
mod semantic_tokens;
mod text;
mod tsc;

View file

@ -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<ImportMap>);
impl ImportMapResolver {
pub fn new(import_map: Arc<ImportMap>) -> Self {
Self(import_map)
}
pub fn as_resolver(&self) -> &dyn Resolver {
self
}
}
impl Resolver for ImportMapResolver {
fn resolve(
&self,
specifier: &str,
referrer: &ModuleSpecifier,
) -> Result<ModuleSpecifier, AnyError> {
self
.0
.resolve(specifier, referrer.as_str())
.map_err(|err| err.into())
}
}

View file

@ -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,
)

View file

@ -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<String, Value>>,
) -> Option<Vec<(ModuleSpecifier, Vec<String>)>> {
let options = maybe_options.as_ref()?;
let types_value = options.get("types")?;
let types: Vec<String> = 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::<Vec<String>>(types_value.clone())
{
imports.extend(types);
}
}
if let Some(jsx_value) = options.get("jsx") {
if let Ok(jsx) = serde_json::from_value::<String>(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::<String>(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<String, Value>>,
) -> Option<String> {
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,
)

View file

@ -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<Mutex<GraphData>>,
pub lockfile: Option<Arc<Mutex<Lockfile>>>,
pub maybe_config_file: Option<ConfigFile>,
pub maybe_import_map: Option<ImportMap>,
pub maybe_import_map: Option<Arc<ImportMap>>,
pub maybe_inspector_server: Option<Arc<InspectorServer>>,
pub root_cert_store: Option<RootCertStore>,
pub blob_store: BlobStore,
@ -200,7 +202,7 @@ impl ProcState {
None
};
let maybe_import_map: Option<ImportMap> =
let maybe_import_map: Option<Arc<ImportMap>> =
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<Vec<(ModuleSpecifier, Vec<String>)>> {
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"?

View file

@ -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<ImportMap>);
impl<'a> ImportMapResolver<'a> {
pub fn new(import_map: &'a ImportMap) -> Self {
impl ImportMapResolver {
pub fn new(import_map: Arc<ImportMap>) -> 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<ImportMapResolver>,
}
impl JsxResolver {
pub fn new(
jsx_import_source_module: String,
maybe_import_map_resolver: Option<ImportMapResolver>,
) -> 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<ModuleSpecifier, AnyError> {
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),
)
}
}

View file

@ -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"
},

View file

@ -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 <A></A>;
}
",
}
}),
);
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&#8203;://localhost:4545/jsx/jsx-runtime\n",
},
"range": {
"start": {
"line": 0,
"character": 21
},
"end": {
"line": 0,
"character": 46
}
}
}))
);
shutdown(&mut client);
}

View file

@ -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()

View file

@ -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",

View file

@ -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 (
<div><></></div>
);
}`,
"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 (
<div><></></div>
);
}`,
"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`,
),
);
},
});

View file

@ -0,0 +1,6 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "jsx"
}
}

6
cli/tests/testdata/jsx/deno-jsx.jsonc vendored Normal file
View file

@ -0,0 +1,6 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "http://localhost:4545/jsx"
}
}

View file

@ -0,0 +1,6 @@
{
"compilerOptions": {
"jsx": "react-jsxdev",
"jsxImportSource": "jsx"
}
}

View file

@ -0,0 +1,6 @@
{
"compilerOptions": {
"jsx": "react-jsxdev",
"jsxImportSource": "http://localhost:4545/jsx"
}
}

View file

@ -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"
}
}

View file

@ -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);

View file

@ -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);

View file

@ -0,0 +1,2 @@
[WILDCARD]
imported http://localhost:4545/jsx/jsx-runtime

View file

@ -0,0 +1,2 @@
[WILDCARD]
imported http://localhost:4545/jsx/jsx-dev-runtime

View file

@ -0,0 +1,2 @@
[WILDCARD]
imported http://localhost:4545/jsx/jsx-runtime/index.ts

View file

@ -0,0 +1,2 @@
[WILDCARD]
imported http://localhost:4545/jsx/jsx-dev-runtime/index.ts

View file

@ -0,0 +1,7 @@
function A() {
return "hello";
}
export function B() {
return <A></A>;
}

View file

@ -0,0 +1,9 @@
/** @jsxImportSource http://localhost:4545/jsx */
function A() {
return "hello";
}
export function B() {
return <A></A>;
}

View file

@ -0,0 +1,9 @@
/** @jsxImportSource jsx */
function A() {
return "hello";
}
export function B() {
return <A></A>;
}

View file

@ -38,7 +38,7 @@ impl Loader for StubDocLoader {
#[derive(Debug)]
struct DocResolver {
import_map: Option<ImportMap>,
import_map: Option<Arc<ImportMap>>,
}
impl Resolver for DocResolver {

View file

@ -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,
},
)?

View file

@ -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,
)

View file

@ -414,6 +414,27 @@ pub struct ResolveArgs {
pub specifiers: Vec<String>,
}
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<Value, AnyError> {
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<Value, AnyError> {
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);
}
}

View file

@ -566,7 +566,9 @@ async fn absolute_redirect(
Ok(file_resp)
}
async fn main_server(req: Request<Body>) -> hyper::Result<Response<Body>> {
async fn main_server(
req: Request<Body>,
) -> Result<Response<Body>, 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<Body>) -> hyper::Result<Response<Body>> {
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<Body>) -> hyper::Result<Response<Body>> {
return Ok(file_resp);
}
return Ok(Response::new(Body::empty()));
Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::empty())
}
};
}