summaryrefslogtreecommitdiff
path: root/cli/tools
diff options
context:
space:
mode:
Diffstat (limited to 'cli/tools')
-rw-r--r--cli/tools/registry/diagnostics.rs32
-rw-r--r--cli/tools/registry/mod.rs23
-rw-r--r--cli/tools/registry/tar.rs8
-rw-r--r--cli/tools/registry/unfurl.rs496
4 files changed, 537 insertions, 22 deletions
diff --git a/cli/tools/registry/diagnostics.rs b/cli/tools/registry/diagnostics.rs
index b605c293b..78cc6f555 100644
--- a/cli/tools/registry/diagnostics.rs
+++ b/cli/tools/registry/diagnostics.rs
@@ -20,7 +20,7 @@ use deno_core::error::AnyError;
use deno_graph::FastCheckDiagnostic;
use lsp_types::Url;
-use crate::util::import_map::ImportMapUnfurlDiagnostic;
+use super::unfurl::SpecifierUnfurlerDiagnostic;
#[derive(Clone, Default)]
pub struct PublishDiagnosticsCollector {
@@ -74,7 +74,7 @@ impl PublishDiagnosticsCollector {
pub enum PublishDiagnostic {
FastCheck(FastCheckDiagnostic),
- ImportMapUnfurl(ImportMapUnfurlDiagnostic),
+ SpecifierUnfurl(SpecifierUnfurlerDiagnostic),
InvalidPath {
path: PathBuf,
message: String,
@@ -102,7 +102,7 @@ impl Diagnostic for PublishDiagnostic {
..
}) => DiagnosticLevel::Warning,
FastCheck(_) => DiagnosticLevel::Error,
- ImportMapUnfurl(_) => DiagnosticLevel::Warning,
+ SpecifierUnfurl(_) => DiagnosticLevel::Warning,
InvalidPath { .. } => DiagnosticLevel::Error,
DuplicatePath { .. } => DiagnosticLevel::Error,
UnsupportedFileType { .. } => DiagnosticLevel::Warning,
@@ -114,7 +114,7 @@ impl Diagnostic for PublishDiagnostic {
use PublishDiagnostic::*;
match &self {
FastCheck(diagnostic) => diagnostic.code(),
- ImportMapUnfurl(diagnostic) => Cow::Borrowed(diagnostic.code()),
+ SpecifierUnfurl(diagnostic) => Cow::Borrowed(diagnostic.code()),
InvalidPath { .. } => Cow::Borrowed("invalid-path"),
DuplicatePath { .. } => Cow::Borrowed("case-insensitive-duplicate-path"),
UnsupportedFileType { .. } => Cow::Borrowed("unsupported-file-type"),
@@ -126,7 +126,7 @@ impl Diagnostic for PublishDiagnostic {
use PublishDiagnostic::*;
match &self {
FastCheck(diagnostic) => diagnostic.message(),
- ImportMapUnfurl(diagnostic) => Cow::Borrowed(diagnostic.message()),
+ SpecifierUnfurl(diagnostic) => Cow::Borrowed(diagnostic.message()),
InvalidPath { message, .. } => Cow::Borrowed(message.as_str()),
DuplicatePath { .. } => {
Cow::Borrowed("package path is a case insensitive duplicate of another path in the package")
@@ -142,8 +142,8 @@ impl Diagnostic for PublishDiagnostic {
use PublishDiagnostic::*;
match &self {
FastCheck(diagnostic) => diagnostic.location(),
- ImportMapUnfurl(diagnostic) => match diagnostic {
- ImportMapUnfurlDiagnostic::UnanalyzableDynamicImport {
+ SpecifierUnfurl(diagnostic) => match diagnostic {
+ SpecifierUnfurlerDiagnostic::UnanalyzableDynamicImport {
specifier,
text_info,
range,
@@ -180,8 +180,8 @@ impl Diagnostic for PublishDiagnostic {
fn snippet(&self) -> Option<DiagnosticSnippet<'_>> {
match &self {
PublishDiagnostic::FastCheck(diagnostic) => diagnostic.snippet(),
- PublishDiagnostic::ImportMapUnfurl(diagnostic) => match diagnostic {
- ImportMapUnfurlDiagnostic::UnanalyzableDynamicImport {
+ PublishDiagnostic::SpecifierUnfurl(diagnostic) => match diagnostic {
+ SpecifierUnfurlerDiagnostic::UnanalyzableDynamicImport {
text_info,
range,
..
@@ -227,7 +227,7 @@ impl Diagnostic for PublishDiagnostic {
fn hint(&self) -> Option<Cow<'_, str>> {
match &self {
PublishDiagnostic::FastCheck(diagnostic) => diagnostic.hint(),
- PublishDiagnostic::ImportMapUnfurl(_) => None,
+ PublishDiagnostic::SpecifierUnfurl(_) => None,
PublishDiagnostic::InvalidPath { .. } => Some(
Cow::Borrowed("rename or remove the file, or add it to 'publish.exclude' in the config file"),
),
@@ -250,11 +250,11 @@ impl Diagnostic for PublishDiagnostic {
PublishDiagnostic::FastCheck(diagnostic) => {
diagnostic.info()
}
- PublishDiagnostic::ImportMapUnfurl(diagnostic) => match diagnostic {
- ImportMapUnfurlDiagnostic::UnanalyzableDynamicImport { .. } => Cow::Borrowed(&[
- Cow::Borrowed("after publishing this package, imports from the local import map do not work"),
+ PublishDiagnostic::SpecifierUnfurl(diagnostic) => match diagnostic {
+ SpecifierUnfurlerDiagnostic::UnanalyzableDynamicImport { .. } => Cow::Borrowed(&[
+ Cow::Borrowed("after publishing this package, imports from the local import map / package.json do not work"),
Cow::Borrowed("dynamic imports that can not be analyzed at publish time will not be rewritten automatically"),
- Cow::Borrowed("make sure the dynamic import is resolvable at runtime without an import map")
+ Cow::Borrowed("make sure the dynamic import is resolvable at runtime without an import map / package.json")
]),
},
PublishDiagnostic::InvalidPath { .. } => Cow::Borrowed(&[
@@ -278,8 +278,8 @@ impl Diagnostic for PublishDiagnostic {
fn docs_url(&self) -> Option<Cow<'_, str>> {
match &self {
PublishDiagnostic::FastCheck(diagnostic) => diagnostic.docs_url(),
- PublishDiagnostic::ImportMapUnfurl(diagnostic) => match diagnostic {
- ImportMapUnfurlDiagnostic::UnanalyzableDynamicImport { .. } => None,
+ PublishDiagnostic::SpecifierUnfurl(diagnostic) => match diagnostic {
+ SpecifierUnfurlerDiagnostic::UnanalyzableDynamicImport { .. } => None,
},
PublishDiagnostic::InvalidPath { .. } => {
Some(Cow::Borrowed("https://jsr.io/go/invalid-path"))
diff --git a/cli/tools/registry/mod.rs b/cli/tools/registry/mod.rs
index 52d303ae1..eadd0e44d 100644
--- a/cli/tools/registry/mod.rs
+++ b/cli/tools/registry/mod.rs
@@ -35,13 +35,13 @@ use crate::factory::CliFactory;
use crate::graph_util::ModuleGraphCreator;
use crate::http_util::HttpClient;
use crate::resolver::MappedSpecifierResolver;
+use crate::resolver::SloppyImportsResolver;
use crate::tools::check::CheckOptions;
use crate::tools::lint::no_slow_types;
use crate::tools::registry::diagnostics::PublishDiagnostic;
use crate::tools::registry::diagnostics::PublishDiagnosticsCollector;
use crate::tools::registry::graph::collect_invalid_external_imports;
use crate::util::display::human_size;
-use crate::util::import_map::ImportMapUnfurler;
mod api;
mod auth;
@@ -50,10 +50,13 @@ mod graph;
mod paths;
mod publish_order;
mod tar;
+mod unfurl;
use auth::get_auth_method;
use auth::AuthMethod;
use publish_order::PublishOrderGraph;
+pub use unfurl::deno_json_deps;
+use unfurl::SpecifierUnfurler;
use super::check::TypeChecker;
@@ -81,12 +84,15 @@ impl PreparedPublishPackage {
static SUGGESTED_ENTRYPOINTS: [&str; 4] =
["mod.ts", "mod.js", "index.ts", "index.js"];
+#[allow(clippy::too_many_arguments)]
async fn prepare_publish(
package_name: &str,
deno_json: &ConfigFile,
source_cache: Arc<ParsedSourceCache>,
graph: Arc<deno_graph::ModuleGraph>,
mapped_resolver: Arc<MappedSpecifierResolver>,
+ sloppy_imports_resolver: Option<SloppyImportsResolver>,
+ bare_node_builtins: bool,
diagnostics_collector: &PublishDiagnosticsCollector,
) -> Result<Rc<PreparedPublishPackage>, AnyError> {
let config_path = deno_json.specifier.to_file_path().unwrap();
@@ -132,7 +138,11 @@ async fn prepare_publish(
let diagnostics_collector = diagnostics_collector.clone();
let tarball = deno_core::unsync::spawn_blocking(move || {
- let unfurler = ImportMapUnfurler::new(&mapped_resolver);
+ let unfurler = SpecifierUnfurler::new(
+ &mapped_resolver,
+ sloppy_imports_resolver.as_ref(),
+ bare_node_builtins,
+ );
tar::create_gzipped_tarball(
&dir_path,
LazyGraphSourceParser::new(&source_cache, &graph),
@@ -661,7 +671,9 @@ async fn prepare_packages_for_publishing(
let module_graph_creator = cli_factory.module_graph_creator().await?.as_ref();
let source_cache = cli_factory.parsed_source_cache();
let type_checker = cli_factory.type_checker().await?;
+ let fs = cli_factory.fs();
let cli_options = cli_factory.cli_options();
+ let bare_node_builtins = cli_options.unstable_bare_node_builtins();
if members.len() > 1 {
println!("Publishing a workspace...");
@@ -686,6 +698,11 @@ async fn prepare_packages_for_publishing(
.into_iter()
.map(|member| {
let mapped_resolver = mapped_resolver.clone();
+ let sloppy_imports_resolver = if cli_options.unstable_sloppy_imports() {
+ Some(SloppyImportsResolver::new(fs.clone()))
+ } else {
+ None
+ };
let graph = graph.clone();
async move {
let package = prepare_publish(
@@ -694,6 +711,8 @@ async fn prepare_packages_for_publishing(
source_cache.clone(),
graph,
mapped_resolver,
+ sloppy_imports_resolver,
+ bare_node_builtins,
diagnostics_collector,
)
.await
diff --git a/cli/tools/registry/tar.rs b/cli/tools/registry/tar.rs
index 66d15b5a6..a8519fe02 100644
--- a/cli/tools/registry/tar.rs
+++ b/cli/tools/registry/tar.rs
@@ -18,10 +18,10 @@ use tar::Header;
use crate::cache::LazyGraphSourceParser;
use crate::tools::registry::paths::PackagePath;
-use crate::util::import_map::ImportMapUnfurler;
use super::diagnostics::PublishDiagnostic;
use super::diagnostics::PublishDiagnosticsCollector;
+use super::unfurl::SpecifierUnfurler;
#[derive(Debug, Clone, PartialEq)]
pub struct PublishableTarballFile {
@@ -40,7 +40,7 @@ pub fn create_gzipped_tarball(
dir: &Path,
source_parser: LazyGraphSourceParser,
diagnostics_collector: &PublishDiagnosticsCollector,
- unfurler: &ImportMapUnfurler,
+ unfurler: &SpecifierUnfurler,
file_patterns: Option<FilePatterns>,
) -> Result<PublishableTarball, AnyError> {
let mut tar = TarGzArchive::new();
@@ -192,7 +192,7 @@ pub fn create_gzipped_tarball(
fn resolve_content_maybe_unfurling(
path: &Path,
specifier: &Url,
- unfurler: &ImportMapUnfurler,
+ unfurler: &SpecifierUnfurler,
source_parser: LazyGraphSourceParser,
diagnostics_collector: &PublishDiagnosticsCollector,
) -> Result<Vec<u8>, AnyError> {
@@ -241,7 +241,7 @@ fn resolve_content_maybe_unfurling(
log::debug!("Unfurling {}", specifier);
let mut reporter = |diagnostic| {
- diagnostics_collector.push(PublishDiagnostic::ImportMapUnfurl(diagnostic));
+ diagnostics_collector.push(PublishDiagnostic::SpecifierUnfurl(diagnostic));
};
let content = unfurler.unfurl(specifier, &parsed_source, &mut reporter);
Ok(content.into_bytes())
diff --git a/cli/tools/registry/unfurl.rs b/cli/tools/registry/unfurl.rs
new file mode 100644
index 000000000..abea2bd0c
--- /dev/null
+++ b/cli/tools/registry/unfurl.rs
@@ -0,0 +1,496 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+use std::collections::HashSet;
+
+use deno_ast::ParsedSource;
+use deno_ast::SourceRange;
+use deno_ast::SourceTextInfo;
+use deno_core::serde_json;
+use deno_core::ModuleSpecifier;
+use deno_graph::DefaultModuleAnalyzer;
+use deno_graph::DependencyDescriptor;
+use deno_graph::DynamicTemplatePart;
+use deno_graph::TypeScriptReference;
+use deno_runtime::deno_node::is_builtin_node_module;
+use deno_semver::jsr::JsrDepPackageReq;
+use deno_semver::jsr::JsrPackageReqReference;
+use deno_semver::npm::NpmPackageReqReference;
+
+use crate::resolver::MappedSpecifierResolver;
+use crate::resolver::SloppyImportsResolver;
+
+pub fn deno_json_deps(
+ config: &deno_config::ConfigFile,
+) -> HashSet<JsrDepPackageReq> {
+ let values = imports_values(config.json.imports.as_ref())
+ .into_iter()
+ .chain(scope_values(config.json.scopes.as_ref()));
+ values_to_set(values)
+}
+
+fn imports_values(value: Option<&serde_json::Value>) -> Vec<&String> {
+ let Some(obj) = value.and_then(|v| v.as_object()) else {
+ return Vec::new();
+ };
+ let mut items = Vec::with_capacity(obj.len());
+ for value in obj.values() {
+ if let serde_json::Value::String(value) = value {
+ items.push(value);
+ }
+ }
+ items
+}
+
+fn scope_values(value: Option<&serde_json::Value>) -> Vec<&String> {
+ let Some(obj) = value.and_then(|v| v.as_object()) else {
+ return Vec::new();
+ };
+ obj.values().flat_map(|v| imports_values(Some(v))).collect()
+}
+
+fn values_to_set<'a>(
+ values: impl Iterator<Item = &'a String>,
+) -> HashSet<JsrDepPackageReq> {
+ let mut entries = HashSet::new();
+ for value in values {
+ if let Ok(req_ref) = JsrPackageReqReference::from_str(value) {
+ entries.insert(JsrDepPackageReq::jsr(req_ref.into_inner().req));
+ } else if let Ok(req_ref) = NpmPackageReqReference::from_str(value) {
+ entries.insert(JsrDepPackageReq::npm(req_ref.into_inner().req));
+ }
+ }
+ entries
+}
+
+#[derive(Debug, Clone)]
+pub enum SpecifierUnfurlerDiagnostic {
+ UnanalyzableDynamicImport {
+ specifier: ModuleSpecifier,
+ text_info: SourceTextInfo,
+ range: SourceRange,
+ },
+}
+
+impl SpecifierUnfurlerDiagnostic {
+ pub fn code(&self) -> &'static str {
+ match self {
+ Self::UnanalyzableDynamicImport { .. } => "unanalyzable-dynamic-import",
+ }
+ }
+
+ pub fn message(&self) -> &'static str {
+ match self {
+ Self::UnanalyzableDynamicImport { .. } => {
+ "unable to analyze dynamic import"
+ }
+ }
+ }
+}
+
+pub struct SpecifierUnfurler<'a> {
+ mapped_resolver: &'a MappedSpecifierResolver,
+ sloppy_imports_resolver: Option<&'a SloppyImportsResolver>,
+ bare_node_builtins: bool,
+}
+
+impl<'a> SpecifierUnfurler<'a> {
+ pub fn new(
+ mapped_resolver: &'a MappedSpecifierResolver,
+ sloppy_imports_resolver: Option<&'a SloppyImportsResolver>,
+ bare_node_builtins: bool,
+ ) -> Self {
+ Self {
+ mapped_resolver,
+ sloppy_imports_resolver,
+ bare_node_builtins,
+ }
+ }
+
+ fn unfurl_specifier(
+ &self,
+ referrer: &ModuleSpecifier,
+ specifier: &str,
+ ) -> Option<String> {
+ let resolved =
+ if let Ok(resolved) = self.mapped_resolver.resolve(specifier, referrer) {
+ resolved.into_specifier()
+ } else {
+ None
+ };
+ let resolved = match resolved {
+ Some(resolved) => resolved,
+ None if self.bare_node_builtins && is_builtin_node_module(specifier) => {
+ format!("node:{specifier}").parse().unwrap()
+ }
+ None => ModuleSpecifier::options()
+ .base_url(Some(referrer))
+ .parse(specifier)
+ .ok()?,
+ };
+ // TODO(lucacasonato): this requires integration in deno_graph first
+ // let resolved = if let Ok(specifier) =
+ // NpmPackageReqReference::from_specifier(&resolved)
+ // {
+ // if let Some(scope_name) = specifier.req().name.strip_prefix("@jsr/") {
+ // let (scope, name) = scope_name.split_once("__")?;
+ // let new_specifier = JsrPackageReqReference::new(PackageReqReference {
+ // req: PackageReq {
+ // name: format!("@{scope}/{name}"),
+ // version_req: specifier.req().version_req.clone(),
+ // },
+ // sub_path: specifier.sub_path().map(ToOwned::to_owned),
+ // })
+ // .to_string();
+ // ModuleSpecifier::parse(&new_specifier).unwrap()
+ // } else {
+ // resolved
+ // }
+ // } else {
+ // resolved
+ // };
+ let resolved =
+ if let Some(sloppy_imports_resolver) = self.sloppy_imports_resolver {
+ sloppy_imports_resolver
+ .resolve(&resolved)
+ .as_specifier()
+ .clone()
+ } else {
+ resolved
+ };
+ relative_url(&resolved, referrer, specifier)
+ }
+
+ /// Attempts to unfurl the dynamic dependency returning `true` on success
+ /// or `false` when the import was not analyzable.
+ fn try_unfurl_dynamic_dep(
+ &self,
+ module_url: &lsp_types::Url,
+ parsed_source: &ParsedSource,
+ dep: &deno_graph::DynamicDependencyDescriptor,
+ text_changes: &mut Vec<deno_ast::TextChange>,
+ ) -> bool {
+ match &dep.argument {
+ deno_graph::DynamicArgument::String(specifier) => {
+ let range = to_range(parsed_source, &dep.argument_range);
+ let maybe_relative_index =
+ parsed_source.text_info().text_str()[range.start..].find(specifier);
+ let Some(relative_index) = maybe_relative_index else {
+ return false;
+ };
+ let unfurled = self.unfurl_specifier(module_url, specifier);
+ let Some(unfurled) = unfurled else {
+ return false;
+ };
+ let start = range.start + relative_index;
+ text_changes.push(deno_ast::TextChange {
+ range: start..start + specifier.len(),
+ new_text: unfurled,
+ });
+ true
+ }
+ deno_graph::DynamicArgument::Template(parts) => match parts.first() {
+ Some(DynamicTemplatePart::String { value: specifier }) => {
+ // relative doesn't need to be modified
+ let is_relative =
+ specifier.starts_with("./") || specifier.starts_with("../");
+ if is_relative {
+ return true;
+ }
+ if !specifier.ends_with('/') {
+ return false;
+ }
+ let unfurled = self.unfurl_specifier(module_url, specifier);
+ let Some(unfurled) = unfurled else {
+ return false;
+ };
+ let range = to_range(parsed_source, &dep.argument_range);
+ let maybe_relative_index =
+ parsed_source.text_info().text_str()[range.start..].find(specifier);
+ let Some(relative_index) = maybe_relative_index else {
+ return false;
+ };
+ let start = range.start + relative_index;
+ text_changes.push(deno_ast::TextChange {
+ range: start..start + specifier.len(),
+ new_text: unfurled,
+ });
+ true
+ }
+ Some(DynamicTemplatePart::Expr) => {
+ false // failed analyzing
+ }
+ None => {
+ true // ignore
+ }
+ },
+ deno_graph::DynamicArgument::Expr => {
+ false // failed analyzing
+ }
+ }
+ }
+
+ pub fn unfurl(
+ &self,
+ url: &ModuleSpecifier,
+ parsed_source: &ParsedSource,
+ diagnostic_reporter: &mut dyn FnMut(SpecifierUnfurlerDiagnostic),
+ ) -> String {
+ let mut text_changes = Vec::new();
+ let module_info = DefaultModuleAnalyzer::module_info(parsed_source);
+ let analyze_specifier =
+ |specifier: &str,
+ range: &deno_graph::PositionRange,
+ text_changes: &mut Vec<deno_ast::TextChange>| {
+ if let Some(unfurled) = self.unfurl_specifier(url, specifier) {
+ text_changes.push(deno_ast::TextChange {
+ range: to_range(parsed_source, range),
+ new_text: unfurled,
+ });
+ }
+ };
+ for dep in &module_info.dependencies {
+ match dep {
+ DependencyDescriptor::Static(dep) => {
+ analyze_specifier(
+ &dep.specifier,
+ &dep.specifier_range,
+ &mut text_changes,
+ );
+ }
+ DependencyDescriptor::Dynamic(dep) => {
+ let success = self.try_unfurl_dynamic_dep(
+ url,
+ parsed_source,
+ dep,
+ &mut text_changes,
+ );
+
+ if !success {
+ let start_pos = parsed_source
+ .text_info()
+ .line_start(dep.argument_range.start.line)
+ + dep.argument_range.start.character;
+ let end_pos = parsed_source
+ .text_info()
+ .line_start(dep.argument_range.end.line)
+ + dep.argument_range.end.character;
+ diagnostic_reporter(
+ SpecifierUnfurlerDiagnostic::UnanalyzableDynamicImport {
+ specifier: url.to_owned(),
+ range: SourceRange::new(start_pos, end_pos),
+ text_info: parsed_source.text_info().clone(),
+ },
+ );
+ }
+ }
+ }
+ }
+ 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,
+ &mut text_changes,
+ );
+ }
+ for specifier_with_range in &module_info.jsdoc_imports {
+ analyze_specifier(
+ &specifier_with_range.text,
+ &specifier_with_range.range,
+ &mut text_changes,
+ );
+ }
+ if let Some(specifier_with_range) = &module_info.jsx_import_source {
+ analyze_specifier(
+ &specifier_with_range.text,
+ &specifier_with_range.range,
+ &mut text_changes,
+ );
+ }
+
+ let rewritten_text = deno_ast::apply_text_changes(
+ parsed_source.text_info().text_str(),
+ text_changes,
+ );
+ rewritten_text
+ }
+}
+
+fn relative_url(
+ resolved: &ModuleSpecifier,
+ referrer: &ModuleSpecifier,
+ specifier: &str,
+) -> Option<String> {
+ let new_specifier = if resolved.scheme() == "file" {
+ format!("./{}", referrer.make_relative(resolved).unwrap())
+ } else {
+ resolved.to_string()
+ };
+ if new_specifier == specifier {
+ return None;
+ }
+ Some(new_specifier)
+}
+
+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 std::sync::Arc;
+
+ use crate::args::package_json::get_local_package_json_version_reqs;
+ use crate::args::PackageJsonDepsProvider;
+
+ use super::*;
+ use deno_ast::MediaType;
+ use deno_ast::ModuleSpecifier;
+ use deno_core::serde_json::json;
+ use deno_core::url::Url;
+ use deno_runtime::deno_fs::RealFs;
+ use deno_runtime::deno_node::PackageJson;
+ use import_map::ImportMapWithDiagnostics;
+ use indexmap::IndexMap;
+ use pretty_assertions::assert_eq;
+ use test_util::testdata_path;
+
+ fn parse_ast(specifier: &Url, source_code: &str) -> ParsedSource {
+ let media_type = MediaType::from_specifier(specifier);
+ deno_ast::parse_module(deno_ast::ParseParams {
+ specifier: specifier.clone(),
+ media_type,
+ capture_tokens: false,
+ maybe_syntax: None,
+ scope_analysis: false,
+ text_info: deno_ast::SourceTextInfo::new(source_code.into()),
+ })
+ .unwrap()
+ }
+
+ #[test]
+ fn test_unfurling() {
+ let cwd = testdata_path().join("unfurl").to_path_buf();
+
+ let deno_json_url =
+ ModuleSpecifier::from_file_path(cwd.join("deno.json")).unwrap();
+ let value = json!({
+ "imports": {
+ "express": "npm:express@5",
+ "lib/": "./lib/",
+ "fizz": "./fizz/mod.ts",
+ "@std/fs": "npm:@jsr/std__fs@1",
+ }
+ });
+ let ImportMapWithDiagnostics { import_map, .. } =
+ import_map::parse_from_value(deno_json_url, value).unwrap();
+ let mut package_json = PackageJson::empty(cwd.join("package.json"));
+ package_json.dependencies =
+ Some(IndexMap::from([("chalk".to_string(), "5".to_string())]));
+ let mapped_resolver = MappedSpecifierResolver::new(
+ Some(Arc::new(import_map)),
+ Arc::new(PackageJsonDepsProvider::new(Some(
+ get_local_package_json_version_reqs(&package_json),
+ ))),
+ );
+
+ let fs = Arc::new(RealFs);
+ let sloppy_imports_resolver = SloppyImportsResolver::new(fs);
+
+ let unfurler = SpecifierUnfurler::new(
+ &mapped_resolver,
+ Some(&sloppy_imports_resolver),
+ true,
+ );
+
+ // 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";
+import chalk from "chalk";
+import baz from "./baz";
+import b from "./b.js";
+import b2 from "./b";
+import url from "url";
+// TODO: unfurl these to jsr
+// import "npm:@jsr/std__fs@1/file";
+// import "npm:@jsr/std__fs@1";
+// import "npm:@jsr/std__fs";
+// import "@std/fs";
+
+const test1 = await import("lib/foo.ts");
+const test2 = await import(`lib/foo.ts`);
+const test3 = await import(`lib/${expr}`);
+const test4 = await import(`./lib/${expr}`);
+// will warn
+const test5 = await import(`lib${expr}`);
+const test6 = await import(`${expr}`);
+"#;
+ let specifier =
+ ModuleSpecifier::from_file_path(cwd.join("mod.ts")).unwrap();
+ let source = parse_ast(&specifier, source_code);
+ let mut d = Vec::new();
+ let mut reporter = |diagnostic| d.push(diagnostic);
+ let unfurled_source = unfurler.unfurl(&specifier, &source, &mut reporter);
+ assert_eq!(d.len(), 2);
+ assert!(
+ matches!(
+ d[0],
+ SpecifierUnfurlerDiagnostic::UnanalyzableDynamicImport { .. }
+ ),
+ "{:?}",
+ d[0]
+ );
+ assert!(
+ matches!(
+ d[1],
+ SpecifierUnfurlerDiagnostic::UnanalyzableDynamicImport { .. }
+ ),
+ "{:?}",
+ d[1]
+ );
+ 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";
+import chalk from "npm:chalk@5";
+import baz from "./baz/index.js";
+import b from "./b.ts";
+import b2 from "./b.ts";
+import url from "node:url";
+// TODO: unfurl these to jsr
+// import "npm:@jsr/std__fs@1/file";
+// import "npm:@jsr/std__fs@1";
+// import "npm:@jsr/std__fs";
+// import "@std/fs";
+
+const test1 = await import("./lib/foo.ts");
+const test2 = await import(`./lib/foo.ts`);
+const test3 = await import(`./lib/${expr}`);
+const test4 = await import(`./lib/${expr}`);
+// will warn
+const test5 = await import(`lib${expr}`);
+const test6 = await import(`${expr}`);
+"#;
+ assert_eq!(unfurled_source, expected_source);
+ }
+ }
+}