diff --git a/lib/deno_graph.generated.js b/lib/deno_graph.generated.js index 995a17528..27ce903fb 100644 --- a/lib/deno_graph.generated.js +++ b/lib/deno_graph.generated.js @@ -221,6 +221,7 @@ function isLikeNone(x) { /** * @param {string} specifier * @param {any} maybe_headers + * @param {string | undefined} maybe_jsx_import_source_module * @param {string} content * @param {Function | undefined} maybe_resolve * @param {Function | undefined} maybe_resolve_types @@ -229,6 +230,7 @@ function isLikeNone(x) { export function parseModule( specifier, maybe_headers, + maybe_jsx_import_source_module, content, maybe_resolve, maybe_resolve_types, @@ -239,18 +241,28 @@ export function parseModule( wasm.__wbindgen_realloc, ); var len0 = WASM_VECTOR_LEN; - var ptr1 = passStringToWasm0( + var ptr1 = isLikeNone(maybe_jsx_import_source_module) + ? 0 + : passStringToWasm0( + maybe_jsx_import_source_module, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc, + ); + var len1 = WASM_VECTOR_LEN; + var ptr2 = passStringToWasm0( content, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc, ); - var len1 = WASM_VECTOR_LEN; + var len2 = WASM_VECTOR_LEN; var ret = wasm.parseModule( ptr0, len0, addHeapObject(maybe_headers), ptr1, len1, + ptr2, + len2, isLikeNone(maybe_resolve) ? 0 : addHeapObject(maybe_resolve), isLikeNone(maybe_resolve_types) ? 0 : addHeapObject(maybe_resolve_types), ); @@ -260,6 +272,7 @@ export function parseModule( /** * @param {any} roots * @param {Function} load + * @param {string | undefined} maybe_jsx_import_source_module * @param {Function | undefined} maybe_cache_info * @param {Function | undefined} maybe_resolve * @param {Function | undefined} maybe_resolve_types @@ -272,6 +285,7 @@ export function parseModule( export function createGraph( roots, load, + maybe_jsx_import_source_module, maybe_cache_info, maybe_resolve, maybe_resolve_types, @@ -280,24 +294,34 @@ export function createGraph( maybe_lockfile_name, maybe_imports, ) { - var ptr0 = isLikeNone(maybe_lockfile_name) + var ptr0 = isLikeNone(maybe_jsx_import_source_module) ? 0 : passStringToWasm0( - maybe_lockfile_name, + maybe_jsx_import_source_module, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc, ); var len0 = WASM_VECTOR_LEN; + var ptr1 = isLikeNone(maybe_lockfile_name) + ? 0 + : passStringToWasm0( + maybe_lockfile_name, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc, + ); + var len1 = WASM_VECTOR_LEN; var ret = wasm.createGraph( addHeapObject(roots), addHeapObject(load), + ptr0, + len0, isLikeNone(maybe_cache_info) ? 0 : addHeapObject(maybe_cache_info), isLikeNone(maybe_resolve) ? 0 : addHeapObject(maybe_resolve), isLikeNone(maybe_resolve_types) ? 0 : addHeapObject(maybe_resolve_types), isLikeNone(maybe_check) ? 0 : addHeapObject(maybe_check), isLikeNone(maybe_get_checksum) ? 0 : addHeapObject(maybe_get_checksum), - ptr0, - len0, + ptr1, + len1, addHeapObject(maybe_imports), ); return takeObject(ret); @@ -779,8 +803,8 @@ const imports = { __wbindgen_rethrow: function (arg0) { throw takeObject(arg0); }, - __wbindgen_closure_wrapper1325: function (arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 262, __wbg_adapter_24); + __wbindgen_closure_wrapper1333: function (arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 266, __wbg_adapter_24); return addHeapObject(ret); }, }, diff --git a/lib/deno_graph_bg.wasm b/lib/deno_graph_bg.wasm index 2de61b8ad..8df2ff583 100644 Binary files a/lib/deno_graph_bg.wasm and b/lib/deno_graph_bg.wasm differ diff --git a/mod.ts b/mod.ts index 3e2282999..b07b83b3f 100644 --- a/mod.ts +++ b/mod.ts @@ -38,6 +38,9 @@ export interface CreateGraphOptions { specifier: string, isDynamic: boolean, ): Promise; + /** When identifying a `@jsxImportSource` pragma, what module name will be + * appended to the import source. This defaults to `jsx-runtime`. */ + jsxImportSourceModule?: string; /** An optional callback that will be called with a URL string of the resource * to provide additional meta data about the resource to enrich the module * graph. */ @@ -131,6 +134,7 @@ export function createGraph( : [rootSpecifiers]; const { load = defaultLoad, + jsxImportSourceModule, cacheInfo, resolve, resolveTypes, @@ -142,6 +146,7 @@ export function createGraph( return jsCreateGraph( rootSpecifiers, load, + jsxImportSourceModule, cacheInfo, resolve, resolveTypes, @@ -157,6 +162,9 @@ export interface ParseModuleOptions { /** For remote resources, a record of headers should be set, where the key's * have been normalized to be lower case values. */ headers?: Record; + /** When identifying a `@jsxImportSource` pragma, what module name will be + * appended to the import source. This defaults to `jsx-runtime`. */ + jsxImportSourceModule?: string; /** An optional callback that allows the default resolution logic of the * module graph to be "overridden". This is intended to allow items like an * import map to be used with the module graph. The callback takes the string @@ -184,10 +192,11 @@ export function parseModule( content: string, options: ParseModuleOptions = {}, ): Module { - const { headers, resolve, resolveTypes } = options; + const { headers, jsxImportSourceModule, resolve, resolveTypes } = options; return jsParseModule( specifier, headers, + jsxImportSourceModule, content, resolve, resolveTypes, diff --git a/src/ast.rs b/src/ast.rs index 33abb2451..db76deb7a 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -23,10 +23,12 @@ use std::collections::HashMap; use std::sync::Arc; lazy_static! { - /// Matched the `@deno-types` pragma. + /// Matches the `@deno-types` pragma. static ref DENO_TYPES_RE: Regex = Regex::new(r#"(?i)^\s*@deno-types\s*=\s*(?:["']([^"']+)["']|(\S+))"#) .unwrap(); + /// Matches the `@jsxImportSource` pragma. + static ref JSX_IMPORT_SOURCE_RE: Regex = Regex::new(r#"(?i)^[\s*]*@jsxImportSource\s+(\S+)"#).unwrap(); /// Matches a `/// ` comment reference. static ref TRIPLE_SLASH_REFERENCE_RE: Regex = Regex::new(r"(?i)^/\s*").unwrap(); @@ -79,6 +81,22 @@ pub fn analyze_deno_types( } } +/// Searches comments for a `@jsxImportSource` pragma on JSX/TSX media types +pub fn analyze_jsx_import_sources( + parsed_source: &ParsedSource, +) -> Option<(String, Span)> { + match parsed_source.media_type() { + MediaType::Jsx | MediaType::Tsx => { + parsed_source.get_leading_comments().iter().find_map(|c| { + let captures = JSX_IMPORT_SOURCE_RE.captures(&c.text)?; + let m = captures.get(1)?; + Some((m.as_str().to_string(), comment_match_to_swc_span(c, &m))) + }) + } + _ => None, + } +} + fn comment_match_to_swc_span(comment: &Comment, m: &Match) -> Span { // the comment text starts after the double slash or slash star, so add 2 let comment_start = comment.span.lo + BytePos(2); @@ -211,11 +229,12 @@ mod tests { #[test] fn test_parse() { let specifier = - ModuleSpecifier::parse("file:///a/test.ts").expect("bad specifier"); + ModuleSpecifier::parse("file:///a/test.tsx").expect("bad specifier"); let source = Arc::new( r#" /// /// + /* @jsxImportSource http://example.com/preact */ import { A, B, @@ -236,7 +255,7 @@ mod tests { .to_string(), ); let parser = DefaultSourceParser::new(); - let result = parser.parse_module(&specifier, source, MediaType::TypeScript); + let result = parser.parse_module(&specifier, source, MediaType::Tsx); assert!(result.is_ok()); let parsed_source = result.unwrap(); let dependencies = analyze_dependencies(&parsed_source); @@ -268,6 +287,13 @@ mod tests { parsed_source.source().span_text(&dep_deno_types.1), "https://deno.land/x/types/react/index.d.ts" ); + + let (specifier, span) = analyze_jsx_import_sources(&parsed_source).unwrap(); + assert_eq!(specifier, "http://example.com/preact"); + assert_eq!( + parsed_source.source().span_text(&span), + "http://example.com/preact" + ); } #[test] diff --git a/src/graph.rs b/src/graph.rs index 9ab451529..ba6afa933 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -3,6 +3,7 @@ use crate::ast; use crate::ast::analyze_deno_types; use crate::ast::analyze_dependencies; +use crate::ast::analyze_jsx_import_sources; use crate::ast::analyze_ts_references; use crate::ast::SourceParser; use crate::module_specifier::resolve_import; @@ -1017,6 +1018,19 @@ pub(crate) fn parse_module_from_ast( } } + // Analyze any JSX Import Source pragma + if let Some((import_source, span)) = analyze_jsx_import_sources(parsed_source) + { + let jsx_import_source_module = maybe_resolver + .map(|r| r.jsx_import_source_module()) + .unwrap_or(DEFAULT_JSX_IMPORT_SOURCE_MODULE); + let specifier = format!("{}/{}", import_source, jsx_import_source_module); + let range = Range::from_swc_span(&module.specifier, parsed_source, &span); + let resolved_dependency = resolve(&specifier, &range, maybe_resolver); + let dep = module.dependencies.entry(specifier).or_default(); + dep.maybe_code = resolved_dependency; + } + // Analyze the X-TypeScript-Types header if module.maybe_types_dependency.is_none() { if let Some(headers) = maybe_headers { diff --git a/src/js_graph.rs b/src/js_graph.rs index 60ad4c6cf..2798ffc50 100644 --- a/src/js_graph.rs +++ b/src/js_graph.rs @@ -12,6 +12,7 @@ use crate::source::LoadFuture; use crate::source::Loader; use crate::source::Locker; use crate::source::Resolver; +use crate::source::DEFAULT_JSX_IMPORT_SOURCE_MODULE; use anyhow::anyhow; use anyhow::Result; @@ -145,16 +146,19 @@ impl Locker for JsLocker { #[derive(Debug)] pub struct JsResolver { + maybe_jsx_import_source_module: Option, maybe_resolve: Option, maybe_resolve_types: Option, } impl JsResolver { pub fn new( + maybe_jsx_import_source_module: Option, maybe_resolve: Option, maybe_resolve_types: Option, ) -> Self { Self { + maybe_jsx_import_source_module, maybe_resolve, maybe_resolve_types, } @@ -168,6 +172,13 @@ struct JsResolveTypesResponse { } impl Resolver for JsResolver { + fn jsx_import_source_module(&self) -> &str { + self + .maybe_jsx_import_source_module + .as_deref() + .unwrap_or(DEFAULT_JSX_IMPORT_SOURCE_MODULE) + } + fn resolve( &self, specifier: &str, diff --git a/src/lib.rs b/src/lib.rs index 8193aa839..ac17768d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,6 +127,7 @@ cfg_if! { pub async fn js_create_graph( roots: JsValue, load: js_sys::Function, + maybe_jsx_import_source_module: Option, maybe_cache_info: Option, maybe_resolve: Option, maybe_resolve_types: Option, @@ -138,8 +139,8 @@ cfg_if! { let roots_vec: Vec = roots.into_serde().map_err(|err| JsValue::from(js_sys::Error::new(&err.to_string())))?; let maybe_imports_map: Option>> = maybe_imports.into_serde().map_err(|err| JsValue::from(js_sys::Error::new(&err.to_string())))?; let mut loader = js_graph::JsLoader::new(load, maybe_cache_info); - let maybe_resolver = if maybe_resolve.is_some() || maybe_resolve_types.is_some() { - Some(js_graph::JsResolver::new(maybe_resolve, maybe_resolve_types)) + let maybe_resolver = if maybe_jsx_import_source_module.is_some() || maybe_resolve.is_some() || maybe_resolve_types.is_some() { + Some(js_graph::JsResolver::new(maybe_jsx_import_source_module, maybe_resolve, maybe_resolve_types)) } else { None }; @@ -184,6 +185,7 @@ cfg_if! { pub fn js_parse_module( specifier: String, maybe_headers: JsValue, + maybe_jsx_import_source_module: Option, content: String, maybe_resolve: Option, maybe_resolve_types: Option, @@ -193,8 +195,8 @@ cfg_if! { .map_err(|err| js_sys::Error::new(&err.to_string()))?; let specifier = module_specifier::ModuleSpecifier::parse(&specifier) .map_err(|err| js_sys::Error::new(&err.to_string()))?; - let maybe_resolver = if maybe_resolve.is_some() || maybe_resolve_types.is_some() { - Some(js_graph::JsResolver::new(maybe_resolve, maybe_resolve_types)) + let maybe_resolver = if maybe_jsx_import_source_module.is_some() || maybe_resolve.is_some() || maybe_resolve_types.is_some() { + Some(js_graph::JsResolver::new(maybe_jsx_import_source_module, maybe_resolve, maybe_resolve_types)) } else { None }; @@ -567,6 +569,90 @@ console.log(a); } } + #[tokio::test] + async fn test_create_graph_jsx_import_source() { + let mut loader = setup( + vec![ + ( + "file:///a/test01.tsx", + Ok(( + "file:///a/test01.tsx", + None, + r#"/* @jsxImportSource https://example.com/preact */ + + export function A() { +
Hello Deno
+ } + "#, + )), + ), + ( + "https://example.com/preact/jsx-runtime", + Ok(( + "https://example.com/preact/jsx-runtime/index.js", + Some(vec![("content-type", "application/javascript")]), + r#"export function jsx() {}"#, + )), + ), + ], + vec![], + ); + let root_specifier = + ModuleSpecifier::parse("file:///a/test01.tsx").expect("bad url"); + let graph = create_graph( + vec![root_specifier.clone()], + false, + None, + &mut loader, + None, + None, + None, + ) + .await; + assert_eq!( + json!(graph), + json!({ + "roots": [ + "file:///a/test01.tsx" + ], + "modules": [ + { + "dependencies": [ + { + "specifier": "https://example.com/preact/jsx-runtime", + "code": { + "specifier": "https://example.com/preact/jsx-runtime", + "span": { + "start": { + "line": 0, + "character": 20 + }, + "end": { + "line": 0, + "character": 46 + } + } + } + } + ], + "mediaType": "TSX", + "size": 159, + "specifier": "file:///a/test01.tsx" + }, + { + "dependencies": [], + "mediaType": "JavaScript", + "size": 24, + "specifier": "https://example.com/preact/jsx-runtime/index.js" + } + ], + "redirects": { + "https://example.com/preact/jsx-runtime": "https://example.com/preact/jsx-runtime/index.js" + } + }) + ); + } + #[tokio::test] async fn test_bare_specifier_error() { let mut loader = setup( @@ -1080,6 +1166,45 @@ console.log(a); assert_eq!(actual.media_type, MediaType::TypeScript); } + #[test] + fn test_parse_module_jsx_import_source() { + let specifier = ModuleSpecifier::parse("file:///a/test01.tsx").unwrap(); + let result = parse_module( + &specifier, + None, + Arc::new( + r#" + /** @jsxImportSource https://example.com/preact */ + + export function A() { + return
Hello Deno
; + } + "# + .to_string(), + ), + None, + None, + ); + assert!(result.is_ok()); + let actual = result.unwrap(); + assert_eq!(actual.dependencies.len(), 1); + let dep = actual + .dependencies + .get("https://example.com/preact/jsx-runtime") + .unwrap(); + assert!(dep.maybe_code.is_some()); + let code_dep = dep.maybe_code.clone().unwrap(); + assert!(code_dep.is_ok()); + let (dep_specifier, _) = code_dep.unwrap(); + assert_eq!( + dep_specifier, + ModuleSpecifier::parse("https://example.com/preact/jsx-runtime").unwrap() + ); + assert!(dep.maybe_type.is_none()); + assert_eq!(actual.specifier, specifier); + assert_eq!(actual.media_type, MediaType::Tsx); + } + #[test] fn test_parse_module_with_headers() { let specifier = ModuleSpecifier::parse("https://localhost/file").unwrap(); diff --git a/src/source.rs b/src/source.rs index d253f853a..4d3d0370d 100644 --- a/src/source.rs +++ b/src/source.rs @@ -21,6 +21,8 @@ use std::path::PathBuf; use std::pin::Pin; use std::sync::Arc; +pub static DEFAULT_JSX_IMPORT_SOURCE_MODULE: &str = "jsx-runtime"; + /// Information that comes from an external source which can be optionally /// included in the module graph. #[derive(Debug, Default, Clone, Deserialize, Serialize)] @@ -90,6 +92,12 @@ pub trait Locker: fmt::Debug { /// dependencies. This can be use to provide import maps and override other /// default resolution logic used by `deno_graph`. pub trait Resolver: fmt::Debug { + /// An optional method which returns the JSX import source module which will + /// be appended to any JSX import source pragmas identified. + fn jsx_import_source_module(&self) -> &str { + DEFAULT_JSX_IMPORT_SOURCE_MODULE + } + /// Given a string specifier and a referring module specifier, return a /// resolved module specifier. fn resolve( diff --git a/test.ts b/test.ts index f1963c535..00a94e3df 100644 --- a/test.ts +++ b/test.ts @@ -266,6 +266,23 @@ Deno.test({ }, }); +Deno.test({ + name: "parseModule() - with jsxImportSource pragma", + fn() { + const module = parseModule( + `file:///a/test01.tsx`, + `/* @jsxImportSource http://example.com/preact */ + export function A() { +
Hello Deno
+ }`, + { + jsxImportSourceModule: "jsx-dev-runtime", + }, + ); + assert(module.dependencies["http://example.com/preact/jsx-dev-runtime"]); + }, +}); + Deno.test({ name: "parseModule() - invalid URL", fn() {