Skip to content

Commit

Permalink
feat(ext): expand scopes and expand when parsing (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsherret authored Feb 22, 2024
1 parent 151e4c6 commit e960415
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 119 deletions.
165 changes: 95 additions & 70 deletions rs-lib/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Value>,
) -> serde_json::Map<String, Value> {
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 {
Expand Down Expand Up @@ -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/",
})
);
}
Expand Down
125 changes: 76 additions & 49 deletions rs-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,19 @@ pub struct ImportMapOptions {
/// `(parsed_address, key, maybe_scope) -> new_address`
#[allow(clippy::type_complexity)]
pub address_hook: Option<Box<dyn (Fn(&str, &str, Option<&str>) -> 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 {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -677,10 +656,30 @@ fn parse_json(
}

fn parse_value(
mut v: Value,
v: Value,
options: &ImportMapOptions,
diagnostics: &mut Vec<ImportMapDiagnostic>,
) -> 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(_) => {}
_ => {
Expand Down Expand Up @@ -1131,6 +1130,7 @@ fn resolve_imports_match(
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;

#[test]
fn npm_specifiers() {
Expand Down Expand Up @@ -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"
}
}
}
"#,
);
}

Expand Down
1 change: 1 addition & 0 deletions rs-lib/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,7 @@ fn parse_with_address_hook() {
}
address.to_string()
})),
expand_imports: false,
},
)
.unwrap();
Expand Down

0 comments on commit e960415

Please sign in to comment.