diff options
Diffstat (limited to 'cli/util/import_map.rs')
-rw-r--r-- | cli/util/import_map.rs | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/cli/util/import_map.rs b/cli/util/import_map.rs new file mode 100644 index 000000000..e8fea1e03 --- /dev/null +++ b/cli/util/import_map.rs @@ -0,0 +1,196 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use deno_ast::ParsedSource; +use deno_core::error::AnyError; +use deno_core::ModuleSpecifier; +use deno_graph::DefaultModuleAnalyzer; +use deno_graph::MediaType; +use deno_graph::TypeScriptReference; +use import_map::ImportMap; + +pub struct ImportMapUnfurler { + import_map: ImportMap, +} + +impl ImportMapUnfurler { + pub fn new(import_map: ImportMap) -> Self { + Self { import_map } + } + + pub fn unfurl( + &self, + url: &ModuleSpecifier, + data: Vec<u8>, + ) -> Result<Vec<u8>, AnyError> { + let media_type = MediaType::from_specifier(url); + + match media_type { + MediaType::JavaScript + | MediaType::Jsx + | MediaType::Mjs + | MediaType::Cjs + | MediaType::TypeScript + | MediaType::Mts + | MediaType::Cts + | MediaType::Dts + | MediaType::Dmts + | MediaType::Dcts + | MediaType::Tsx => { + // continue + } + MediaType::SourceMap + | MediaType::Unknown + | MediaType::Json + | MediaType::Wasm + | MediaType::TsBuildInfo => { + // not unfurlable data + return Ok(data); + } + } + + let text = String::from_utf8(data)?; + let parsed_source = deno_ast::parse_module(deno_ast::ParseParams { + specifier: url.to_string(), + text_info: deno_ast::SourceTextInfo::from_string(text), + media_type, + capture_tokens: false, + maybe_syntax: None, + scope_analysis: false, + })?; + let mut text_changes = Vec::new(); + let module_info = DefaultModuleAnalyzer::module_info(&parsed_source); + let mut analyze_specifier = + |specifier: &str, range: &deno_graph::PositionRange| { + let resolved = self.import_map.resolve(specifier, url); + if let Ok(resolved) = resolved { + let new_text = if resolved.scheme() == "file" { + format!("./{}", url.make_relative(&resolved).unwrap()) + } else { + resolved.to_string() + }; + text_changes.push(deno_ast::TextChange { + range: to_range(&parsed_source, range), + new_text, + }); + } + }; + for dep in &module_info.dependencies { + analyze_specifier(&dep.specifier, &dep.specifier_range); + } + for ts_ref in &module_info.ts_references { + let specifier_with_range = match ts_ref { + TypeScriptReference::Path(range) => range, + TypeScriptReference::Types(range) => range, + }; + analyze_specifier( + &specifier_with_range.text, + &specifier_with_range.range, + ); + } + for specifier_with_range in &module_info.jsdoc_imports { + analyze_specifier( + &specifier_with_range.text, + &specifier_with_range.range, + ); + } + if let Some(specifier_with_range) = &module_info.jsx_import_source { + analyze_specifier( + &specifier_with_range.text, + &specifier_with_range.range, + ); + } + Ok( + deno_ast::apply_text_changes( + parsed_source.text_info().text_str(), + text_changes, + ) + .into_bytes(), + ) + } + + #[cfg(test)] + fn unfurl_to_string( + &self, + url: &ModuleSpecifier, + data: Vec<u8>, + ) -> Result<String, AnyError> { + let data = self.unfurl(url, data)?; + let content = String::from_utf8(data)?; + Ok(content) + } +} + +fn to_range( + parsed_source: &ParsedSource, + range: &deno_graph::PositionRange, +) -> std::ops::Range<usize> { + let mut range = range + .as_source_range(parsed_source.text_info()) + .as_byte_range(parsed_source.text_info().range().start); + let text = &parsed_source.text_info().text_str()[range.clone()]; + if text.starts_with('"') || text.starts_with('\'') { + range.start += 1; + } + if text.ends_with('"') || text.ends_with('\'') { + range.end -= 1; + } + range +} + +#[cfg(test)] +mod tests { + use super::*; + use deno_ast::ModuleSpecifier; + use deno_core::serde_json::json; + use import_map::ImportMapWithDiagnostics; + use pretty_assertions::assert_eq; + + #[test] + fn test_unfurling() { + let deno_json_url = + ModuleSpecifier::parse("file:///dev/deno.json").unwrap(); + let value = json!({ + "imports": { + "express": "npm:express@5", + "lib/": "./lib/", + "fizz": "./fizz/mod.ts" + } + }); + let ImportMapWithDiagnostics { import_map, .. } = + import_map::parse_from_value(&deno_json_url, value).unwrap(); + let unfurler = ImportMapUnfurler::new(import_map); + + // Unfurling TS file should apply changes. + { + let source_code = r#"import express from "express";" +import foo from "lib/foo.ts"; +import bar from "lib/bar.ts"; +import fizz from "fizz"; +"#; + let specifier = ModuleSpecifier::parse("file:///dev/mod.ts").unwrap(); + let unfurled_source = unfurler + .unfurl_to_string(&specifier, source_code.as_bytes().to_vec()) + .unwrap(); + let expected_source = r#"import express from "npm:express@5";" +import foo from "./lib/foo.ts"; +import bar from "./lib/bar.ts"; +import fizz from "./fizz/mod.ts"; +"#; + assert_eq!(unfurled_source, expected_source); + } + + // Unfurling file with "unknown" media type should leave it as is + { + let source_code = r#"import express from "express";" +import foo from "lib/foo.ts"; +import bar from "lib/bar.ts"; +import fizz from "fizz"; +"#; + let specifier = ModuleSpecifier::parse("file:///dev/mod").unwrap(); + let unfurled_source = unfurler + .unfurl_to_string(&specifier, source_code.as_bytes().to_vec()) + .unwrap(); + assert_eq!(unfurled_source, source_code); + } + } +} |