diff --git a/rs-lib/src/ext.rs b/rs-lib/src/ext.rs index 028587c..196e221 100644 --- a/rs-lib/src/ext.rs +++ b/rs-lib/src/ext.rs @@ -4,64 +4,89 @@ use serde_json::json; use serde_json::Value; use url::Url; -/// This function can be used to modify the `import` mapping to expand -/// bare specifier imports to provide "directory" imports, eg.: +/// This function can be used to modify the `imports` and "scopes" mappings +/// to expand bare specifier imports to provide "directory" imports, eg.: /// - `"express": "npm:express@4` -> `"express/": "npm:/express@4/` /// - `"@std": "jsr:@std` -> `"std@/": "jsr:/@std/` /// /// Only `npm:` and `jsr:` scheme are expanded and if there's already a /// "directory" import, it is not overwritten. -pub fn expand_imports(import_map: ImportMapConfig) -> Value { - let mut expanded_imports = serde_json::Map::new(); +pub fn expand_import_map_value(import_map: Value) -> Value { + let Value::Object(mut import_map) = import_map else { + return import_map; + }; - let imports = import_map - .import_map_value - .get("imports") - .cloned() - .unwrap_or_else(|| Value::Null); + if let Some(imports) = import_map.get("imports").and_then(|i| i.as_object()) { + import_map.insert( + "imports".to_string(), + Value::Object(expand_imports(imports)), + ); + } + if let Some(scopes) = import_map.remove("scopes") { + match scopes { + Value::Object(scopes) => { + let mut expanded_scopes = serde_json::Map::with_capacity(scopes.len()); + for (key, imports) in scopes { + let imports = match imports { + Value::Object(imports) => Value::Object(expand_imports(&imports)), + _ => imports, + }; + expanded_scopes.insert(key, imports); + } + import_map.insert("scopes".to_string(), Value::Object(expanded_scopes)); + } + _ => { + import_map.insert("scopes".to_string(), scopes); + } + } + } - if let Some(imports_map) = imports.as_object() { - for (key, value) in imports_map { - if !key.ends_with('/') { - expanded_imports.insert(key.to_string(), value.clone()); - let key_with_trailing_slash = format!("{}/", key); + Value::Object(import_map) +} - // Don't overwrite existing keys - if imports_map.contains_key(&key_with_trailing_slash) { - continue; - } +fn expand_imports( + imports_map: &serde_json::Map, +) -> serde_json::Map { + let mut expanded_imports = serde_json::Map::new(); + for (key, value) in imports_map { + if !key.ends_with('/') { + expanded_imports.insert(key.to_string(), value.clone()); + let key_with_trailing_slash = format!("{}/", key); - let Some(value_str) = value.as_str() else { - continue; - }; + // Don't overwrite existing keys + if imports_map.contains_key(&key_with_trailing_slash) { + continue; + } - if !value_str.ends_with('/') { - let value_with_trailing_slash = - if let Some(value_str) = value_str.strip_prefix("jsr:") { - let value_str = value_str.strip_prefix('/').unwrap_or(value_str); - Some(format!("jsr:/{}/", value_str)) - } else if let Some(value_str) = value_str.strip_prefix("npm:") { - let value_str = value_str.strip_prefix('/').unwrap_or(value_str); - Some(format!("npm:/{}/", value_str)) - } else { - None - }; + let Some(value_str) = value.as_str() else { + continue; + }; - if let Some(value_with_trailing_slash) = value_with_trailing_slash { - expanded_imports.insert( - key_with_trailing_slash, - Value::String(value_with_trailing_slash), - ); - continue; - } + if !value_str.ends_with('/') { + let value_with_trailing_slash = + if let Some(value_str) = value_str.strip_prefix("jsr:") { + let value_str = value_str.strip_prefix('/').unwrap_or(value_str); + Some(format!("jsr:/{}/", value_str)) + } else if let Some(value_str) = value_str.strip_prefix("npm:") { + let value_str = value_str.strip_prefix('/').unwrap_or(value_str); + Some(format!("npm:/{}/", value_str)) + } else { + None + }; + + if let Some(value_with_trailing_slash) = value_with_trailing_slash { + expanded_imports.insert( + key_with_trailing_slash, + Value::String(value_with_trailing_slash), + ); + continue; } } - - expanded_imports.insert(key.to_string(), value.clone()); } - } - Value::Object(expanded_imports) + expanded_imports.insert(key.to_string(), value.clone()); + } + expanded_imports } pub struct ImportMapConfig { @@ -374,51 +399,51 @@ mod tests { #[test] fn test_expand_imports() { - let import_map = ImportMapConfig { - base_url: Url::parse("file:///import_map.json").unwrap(), - import_map_value: json!({ + let import_map = json!({ + "imports": { + "@std": "jsr:/@std", + "@foo": "jsr:@foo", + "express": "npm:express@4", + "foo": "https://example.com/foo/bar" + }, + "scopes": {} + }); + let value = expand_import_map_value(import_map); + assert_eq!( + value, + json!({ "imports": { "@std": "jsr:/@std", + "@std/": "jsr:/@std/", "@foo": "jsr:@foo", + "@foo/": "jsr:/@foo/", "express": "npm:express@4", + "express/": "npm:/express@4/", "foo": "https://example.com/foo/bar" }, "scopes": {} - }), - }; - let value = expand_imports(import_map); - assert_eq!( - value, - json!({ - "@std": "jsr:/@std", - "@std/": "jsr:/@std/", - "@foo": "jsr:@foo", - "@foo/": "jsr:/@foo/", - "express": "npm:express@4", - "express/": "npm:/express@4/", - "foo": "https://example.com/foo/bar" }) ); } #[test] fn test_expand_imports_with_trailing_slash() { - let import_map = ImportMapConfig { - base_url: Url::parse("file:///import_map.json").unwrap(), - import_map_value: json!({ + let import_map = json!({ + "imports": { + "express": "npm:express@4", + "express/": "npm:/express@4/foo/bar/", + }, + "scopes": {} + }); + let value = expand_import_map_value(import_map); + assert_eq!( + value, + json!({ "imports": { "express": "npm:express@4", "express/": "npm:/express@4/foo/bar/", }, "scopes": {} - }), - }; - let value = expand_imports(import_map); - assert_eq!( - value, - json!({ - "express": "npm:express@4", - "express/": "npm:/express@4/foo/bar/", }) ); } diff --git a/rs-lib/src/lib.rs b/rs-lib/src/lib.rs index af3c6f6..74ba5c4 100644 --- a/rs-lib/src/lib.rs +++ b/rs-lib/src/lib.rs @@ -308,6 +308,19 @@ pub struct ImportMapOptions { /// `(parsed_address, key, maybe_scope) -> new_address` #[allow(clippy::type_complexity)] pub address_hook: Option) -> String)>>, + /// Whether to expand imports in the import map. + /// + /// This functionality can be used to modify the import map + /// during parsing, by changing the `imports` mapping to expand + /// bare specifier imports to provide "directory" imports, eg.: + /// - `"express": "npm:express@4` -> `"express/": "npm:/express@4/` + /// - `"@std": "jsr:@std` -> `"std@/": "jsr:/@std/` + /// + /// Only `npm:` and `jsr:` schemes are expanded and if there's already a + /// "directory" import, it is not overwritten. + /// + /// This requires enabling the "ext" cargo feature. + pub expand_imports: bool, } impl Debug for ImportMapOptions { @@ -565,40 +578,6 @@ impl ImportMap { text.replace('"', "\\\"") } } - - /// This function can be used to modify the import map in place, - /// by modifying `imports` mapping to expand - /// bare specifier imports to provide "directory" imports, eg.: - /// - `"express": "npm:express@4` -> `"express/": "npm:/express@4/` - /// - `"@std": "jsr:@std` -> `"std@/": "jsr:/@std/` - /// - /// Only `npm:` and `jsr:` scheme are expanded and if there's already a - /// "directory" import, it is not overwritten. - #[cfg(feature = "ext")] - pub fn ext_expand_imports(&mut self) { - use ext::ImportMapConfig; - - let json_str = self.to_json(); - let json_value = serde_json::from_str(&json_str).unwrap(); - let expanded_imports = ext::expand_imports(ImportMapConfig { - base_url: self.base_url.clone(), - import_map_value: json_value, - }); - let expanded_imports_map = expanded_imports.as_object().unwrap(); - let mut expanded_imports_im = - IndexMap::with_capacity(expanded_imports_map.len()); - for (key, value) in expanded_imports_map { - expanded_imports_im - .insert(key.to_string(), Some(value.as_str().unwrap().to_string())); - } - let mut diagnostics = vec![]; - let imports = parse_specifier_map( - expanded_imports_im, - &self.base_url, - &mut diagnostics, - ); - self.imports = imports; - } } pub fn parse_from_json( @@ -677,10 +656,30 @@ fn parse_json( } fn parse_value( - mut v: Value, + v: Value, options: &ImportMapOptions, diagnostics: &mut Vec, ) -> Result<(UnresolvedSpecifierMap, UnresolvedScopesMap), ImportMapError> { + fn maybe_expand_imports(value: Value, options: &ImportMapOptions) -> Value { + if options.expand_imports { + #[cfg(feature = "ext")] + { + ext::expand_import_map_value(value) + } + #[cfg(not(feature = "ext"))] + { + debug_assert!( + false, + "expand_imports was true, but the \"ext\" feature was not enabled" + ); + value + } + } else { + value + } + } + + let mut v = maybe_expand_imports(v, options); match v { Value::Object(_) => {} _ => { @@ -1131,6 +1130,7 @@ fn resolve_imports_match( #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn npm_specifiers() { @@ -1216,22 +1216,49 @@ mod test { "express": "npm:express@4", "foo": "https://example.com/foo/bar" }, - "scopes": {} + "scopes": { + "./folder/": { + "@std": "jsr:/@std", + "@foo": "jsr:@foo", + "express": "npm:express@4", + "foo": "https://example.com/foo/bar" + } + } }"#; - let im = parse_from_json(&url, json_string).unwrap(); - let mut im = im.import_map; - im.ext_expand_imports(); + let im = parse_from_json_with_options( + &url, + json_string, + ImportMapOptions { + address_hook: None, + expand_imports: true, + }, + ) + .unwrap(); assert_eq!( - serde_json::to_value(&im.imports).unwrap(), - serde_json::json!({ - "@std": "jsr:/@std", - "@std/": "jsr:/@std/", - "@foo": "jsr:@foo", - "@foo/": "jsr:/@foo/", - "express": "npm:express@4", - "express/": "npm:/express@4/", - "foo": "https://example.com/foo/bar" - }) + im.import_map.to_json(), + r#"{ + "imports": { + "@std": "jsr:/@std", + "@std/": "jsr:/@std/", + "@foo": "jsr:@foo", + "@foo/": "jsr:/@foo/", + "express": "npm:express@4", + "express/": "npm:/express@4/", + "foo": "https://example.com/foo/bar" + }, + "scopes": { + "./folder/": { + "@std": "jsr:/@std", + "@std/": "jsr:/@std/", + "@foo": "jsr:@foo", + "@foo/": "jsr:/@foo/", + "express": "npm:express@4", + "express/": "npm:/express@4/", + "foo": "https://example.com/foo/bar" + } + } +} +"#, ); } diff --git a/rs-lib/tests/integration_test.rs b/rs-lib/tests/integration_test.rs index 41d7d4a..b74f83c 100644 --- a/rs-lib/tests/integration_test.rs +++ b/rs-lib/tests/integration_test.rs @@ -697,6 +697,7 @@ fn parse_with_address_hook() { } address.to_string() })), + expand_imports: false, }, ) .unwrap();