Repository: denoland/deno
Pull request: fix(publish): unfurl import.meta.resolve calls

Closes #30659

Changed files:
filename: cli/tools/publish/module_content.rs
patch: @@ -22,6 +22,7 @@ use sys_traits::FsRead;
 
 use super::diagnostics::PublishDiagnostic;
 use super::diagnostics::PublishDiagnosticsCollector;
+use super::unfurl::PositionOrSourceRangeRef;
 use super::unfurl::SpecifierUnfurler;
 use super::unfurl::SpecifierUnfurlerDiagnostic;
 use crate::sys::CliSys;
@@ -249,7 +250,9 @@ impl<TSys: FsMetadata + FsRead> ModuleContentProvider<TSys> {
             import_source,
             resolution_kind,
             text_info,
-            &deno_graph::PositionRange::zeroed(),
+            PositionOrSourceRangeRef::PositionRange(
+              &deno_graph::PositionRange::zeroed(),
+            ),
             diagnostic_reporter,
           );
         maybe_import_source.unwrap_or_else(|| import_source.to_string())

filename: cli/tools/publish/unfurl.rs
patch: @@ -4,7 +4,9 @@ use std::borrow::Cow;
 use std::sync::Arc;
 
 use deno_ast::ParsedSource;
+use deno_ast::SourcePos;
 use deno_ast::SourceRange;
+use deno_ast::SourceRangedForSpanned;
 use deno_ast::SourceTextInfo;
 use deno_ast::SourceTextProvider;
 use deno_ast::TextChange;
@@ -16,6 +18,15 @@ use deno_ast::diagnostics::DiagnosticSnippetHighlight;
 use deno_ast::diagnostics::DiagnosticSnippetHighlightStyle;
 use deno_ast::diagnostics::DiagnosticSourcePos;
 use deno_ast::diagnostics::DiagnosticSourceRange;
+use deno_ast::swc::ast::Callee;
+use deno_ast::swc::ast::Expr;
+use deno_ast::swc::ast::Lit;
+use deno_ast::swc::ast::MemberProp;
+use deno_ast::swc::ast::MetaPropKind;
+use deno_ast::swc::atoms::Atom;
+use deno_ast::swc::ecma_visit::Visit;
+use deno_ast::swc::ecma_visit::VisitWith;
+use deno_ast::swc::ecma_visit::noop_visit_type;
 use deno_core::ModuleSpecifier;
 use deno_core::anyhow;
 use deno_graph::analysis::DependencyDescriptor;
@@ -43,6 +54,11 @@ pub enum SpecifierUnfurlerDiagnostic {
     text_info: SourceTextInfo,
     range: SourceRange,
   },
+  UnanalyzableImportMetaResolve {
+    specifier: ModuleSpecifier,
+    text_info: SourceTextInfo,
+    range: SourceRange,
+  },
   ResolvingNpmWorkspacePackage {
     specifier: ModuleSpecifier,
     package_name: String,
@@ -70,6 +86,9 @@ impl Diagnostic for SpecifierUnfurlerDiagnostic {
       SpecifierUnfurlerDiagnostic::UnanalyzableDynamicImport { .. } => {
         DiagnosticLevel::Warning
       }
+      SpecifierUnfurlerDiagnostic::UnanalyzableImportMetaResolve { .. } => {
+        DiagnosticLevel::Warning
+      }
       SpecifierUnfurlerDiagnostic::ResolvingNpmWorkspacePackage { .. } => {
         DiagnosticLevel::Error
       }
@@ -85,6 +104,9 @@ impl Diagnostic for SpecifierUnfurlerDiagnostic {
   fn code(&self) -> Cow<'_, str> {
     match self {
       Self::UnanalyzableDynamicImport { .. } => "unanalyzable-dynamic-import",
+      Self::UnanalyzableImportMetaResolve { .. } => {
+        "unanalyzable-import-meta-resolve"
+      }
       Self::ResolvingNpmWorkspacePackage { .. } => "npm-workspace-package",
       Self::UnsupportedPkgJsonFileSpecifier { .. } => {
         "unsupported-file-specifier"
@@ -101,6 +123,9 @@ impl Diagnostic for SpecifierUnfurlerDiagnostic {
       Self::UnanalyzableDynamicImport { .. } => {
         "unable to analyze dynamic import".into()
       }
+      Self::UnanalyzableImportMetaResolve { .. } => {
+        "unable to analyze import.meta.resolve".into()
+      }
       Self::ResolvingNpmWorkspacePackage {
         package_name,
         reason,
@@ -134,6 +159,15 @@ impl Diagnostic for SpecifierUnfurlerDiagnostic {
         text_info: Cow::Borrowed(text_info),
         source_pos: DiagnosticSourcePos::SourcePos(range.start),
       },
+      SpecifierUnfurlerDiagnostic::UnanalyzableImportMetaResolve {
+        specifier,
+        text_info,
+        range,
+      } => DiagnosticLocation::ModulePosition {
+        specifier: Cow::Borrowed(specifier),
+        text_info: Cow::Borrowed(text_info),
+        source_pos: DiagnosticSourcePos::SourcePos(range.start),
+      },
       SpecifierUnfurlerDiagnostic::ResolvingNpmWorkspacePackage {
         specifier,
         text_info,
@@ -184,6 +218,21 @@ impl Diagnostic for SpecifierUnfurlerDiagnostic {
           description: Some("the unanalyzable dynamic import".into()),
         }],
       }),
+      SpecifierUnfurlerDiagnostic::UnanalyzableImportMetaResolve {
+        text_info,
+        range,
+        ..
+      } => Some(DiagnosticSnippet {
+        source: Cow::Borrowed(text_info),
+        highlights: vec![DiagnosticSnippetHighlight {
+          style: DiagnosticSnippetHighlightStyle::Warning,
+          range: DiagnosticSourceRange {
+            start: DiagnosticSourcePos::SourcePos(range.start),
+            end: DiagnosticSourcePos::SourcePos(range.end),
+          },
+          description: Some("the unanalyzable import.meta.resolve call".into()),
+        }],
+      }),
       SpecifierUnfurlerDiagnostic::ResolvingNpmWorkspacePackage {
         text_info,
         range,
@@ -234,7 +283,7 @@ impl Diagnostic for SpecifierUnfurlerDiagnostic {
 
   fn hint(&self) -> Option<Cow<'_, str>> {
     match self {
-      SpecifierUnfurlerDiagnostic::UnanalyzableDynamicImport { .. } => {
+      SpecifierUnfurlerDiagnostic::UnanalyzableDynamicImport { .. } | SpecifierUnfurlerDiagnostic::UnanalyzableImportMetaResolve { .. } => {
         None
       }
       SpecifierUnfurlerDiagnostic::ResolvingNpmWorkspacePackage { .. } => Some(
@@ -270,6 +319,19 @@ impl Diagnostic for SpecifierUnfurlerDiagnostic {
           ),
         ])
       }
+      SpecifierUnfurlerDiagnostic::UnanalyzableImportMetaResolve { .. } => {
+        Cow::Borrowed(&[
+          Cow::Borrowed(
+            "after publishing this package, import.meta.resolve calls from the local import map / package.json do not work",
+          ),
+          Cow::Borrowed(
+            "import.meta.resolve calls that can not be analyzed at publish time will not be rewritten automatically",
+          ),
+          Cow::Borrowed(
+            "make sure the import.meta.resolve call is resolvable at runtime without an import map / package.json",
+          ),
+        ])
+      }
       SpecifierUnfurlerDiagnostic::ResolvingNpmWorkspacePackage { .. } => {
         Cow::Borrowed(&[])
       }
@@ -300,14 +362,23 @@ enum UnfurlSpecifierError {
   },
 }
 
-pub struct SpecifierUnfurler<TSys: FsMetadata + FsRead = CliSys> {
+#[derive(Copy, Clone)]
+pub enum PositionOrSourceRangeRef<'a> {
+  PositionRange(&'a deno_graph::PositionRange),
+  SourceRange(SourceRange<SourcePos>),
+}
+
+#[sys_traits::auto_impl]
+pub trait SpecifierUnfurlerSys: FsMetadata + FsRead {}
+
+pub struct SpecifierUnfurler<TSys: SpecifierUnfurlerSys = CliSys> {
   // optional for testing
   node_resolver: Option<Arc<CliNodeResolver>>,
   workspace_resolver: Arc<WorkspaceResolver<TSys>>,
   bare_node_builtins: bool,
 }
 
-impl<TSys: FsMetadata + FsRead> SpecifierUnfurler<TSys> {
+impl<TSys: SpecifierUnfurlerSys> SpecifierUnfurler<TSys> {
   pub fn new(
     node_resolver: Option<Arc<CliNodeResolver>>,
     workspace_resolver: Arc<WorkspaceResolver<TSys>>,
@@ -330,17 +401,22 @@ impl<TSys: FsMetadata + FsRead> SpecifierUnfurler<TSys> {
     specifier: &str,
     resolution_kind: deno_resolver::workspace::ResolutionKind,
     text_info: &SourceTextInfo,
-    range: &deno_graph::PositionRange,
+    range: PositionOrSourceRangeRef<'_>,
     diagnostic_reporter: &mut dyn FnMut(SpecifierUnfurlerDiagnostic),
   ) -> Option<String> {
     match self.unfurl_specifier(referrer, specifier, resolution_kind) {
       Ok(maybe_unfurled) => maybe_unfurled,
       Err(diagnostic) => {
-        let range = to_range(text_info, range);
-        let range = SourceRange::new(
-          text_info.start_pos() + range.start,
-          text_info.start_pos() + range.end,
-        );
+        let range = match range {
+          PositionOrSourceRangeRef::PositionRange(position_range) => {
+            let range = to_range(text_info, position_range);
+            SourceRange::new(
+              text_info.start_pos() + range.start,
+              text_info.start_pos() + range.end,
+            )
+          }
+          PositionOrSourceRangeRef::SourceRange(source_range) => source_range,
+        };
         match diagnostic {
           UnfurlSpecifierError::UnsupportedPkgJsonFileSpecifier {
             package_name,
@@ -626,7 +702,7 @@ impl<TSys: FsMetadata + FsRead> SpecifierUnfurler<TSys> {
           specifier,
           deno_resolver::workspace::ResolutionKind::Execution, // dynamic imports are always execution
           text_info,
-          &dep.argument_range,
+          PositionOrSourceRangeRef::PositionRange(&dep.argument_range),
           diagnostic_reporter,
         );
         if let Some(unfurled) = maybe_unfurled {
@@ -655,7 +731,7 @@ impl<TSys: FsMetadata + FsRead> SpecifierUnfurler<TSys> {
               specifier,
               deno_resolver::workspace::ResolutionKind::Execution, // dynamic imports are always execution
               text_info,
-              &dep.argument_range,
+              PositionOrSourceRangeRef::PositionRange(&dep.argument_range),
               diagnostic_reporter,
             );
             let Some(unfurled) = unfurled else {
@@ -699,7 +775,7 @@ impl<TSys: FsMetadata + FsRead> SpecifierUnfurler<TSys> {
     let text_info = parsed_source.text_info_lazy();
     let analyze_specifier =
       |specifier: &str,
-       range: &deno_graph::PositionRange,
+       range: PositionOrSourceRangeRef,
        resolution_kind: deno_resolver::workspace::ResolutionKind,
        text_changes: &mut Vec<deno_ast::TextChange>,
        diagnostic_reporter: &mut dyn FnMut(SpecifierUnfurlerDiagnostic)| {
@@ -712,7 +788,14 @@ impl<TSys: FsMetadata + FsRead> SpecifierUnfurler<TSys> {
           diagnostic_reporter,
         ) {
           text_changes.push(deno_ast::TextChange {
-            range: to_range(text_info, range),
+            range: match range {
+              PositionOrSourceRangeRef::PositionRange(position_range) => {
+                to_range(text_info, position_range)
+              }
+              PositionOrSourceRangeRef::SourceRange(source_range) => {
+                source_range.as_byte_range(parsed_source.start_pos())
+              }
+            },
             new_text: unfurled,
           });
         }
@@ -739,7 +822,7 @@ impl<TSys: FsMetadata + FsRead> SpecifierUnfurler<TSys> {
 
           analyze_specifier(
             &dep.specifier,
-            &dep.specifier_range,
+            PositionOrSourceRangeRef::PositionRange(&dep.specifier_range),
             resolution_kind,
             text_changes,
             diagnostic_reporter,
@@ -777,7 +860,7 @@ impl<TSys: FsMetadata + FsRead> SpecifierUnfurler<TSys> {
       };
       analyze_specifier(
         &specifier_with_range.text,
-        &specifier_with_range.range,
+        PositionOrSourceRangeRef::PositionRange(&specifier_with_range.range),
         deno_resolver::workspace::ResolutionKind::Types,
         text_changes,
         diagnostic_reporter,
@@ -786,7 +869,7 @@ impl<TSys: FsMetadata + FsRead> SpecifierUnfurler<TSys> {
     for jsdoc in &module_info.jsdoc_imports {
       analyze_specifier(
         &jsdoc.specifier.text,
-        &jsdoc.specifier.range,
+        PositionOrSourceRangeRef::PositionRange(&jsdoc.specifier.range),
         deno_resolver::workspace::ResolutionKind::Types,
         text_changes,
         diagnostic_reporter,
@@ -795,7 +878,7 @@ impl<TSys: FsMetadata + FsRead> SpecifierUnfurler<TSys> {
     if let Some(specifier_with_range) = &module_info.jsx_import_source {
       analyze_specifier(
         &specifier_with_range.text,
-        &specifier_with_range.range,
+        PositionOrSourceRangeRef::PositionRange(&specifier_with_range.range),
         deno_resolver::workspace::ResolutionKind::Execution,
         text_changes,
         diagnostic_reporter,
@@ -804,12 +887,33 @@ impl<TSys: FsMetadata + FsRead> SpecifierUnfurler<TSys> {
     if let Some(specifier_with_range) = &module_info.jsx_import_source_types {
       analyze_specifier(
         &specifier_with_range.text,
-        &specifier_with_range.range,
+        PositionOrSourceRangeRef::PositionRange(&specifier_with_range.range),
         deno_resolver::workspace::ResolutionKind::Types,
         text_changes,
         diagnostic_reporter,
       );
     }
+
+    let mut collector = ImportMetaResolveCollector::default();
+    parsed_source.program().visit_with(&mut collector);
+    for (range, specifier) in collector.specifiers {
+      analyze_specifier(
+        &specifier,
+        PositionOrSourceRangeRef::SourceRange(range),
+        deno_resolver::workspace::ResolutionKind::Execution,
+        text_changes,
+        diagnostic_reporter,
+      );
+    }
+    for range in collector.diagnostic_ranges {
+      diagnostic_reporter(
+        SpecifierUnfurlerDiagnostic::UnanalyzableImportMetaResolve {
+          specifier: url.to_owned(),
+          range,
+          text_info: text_info.clone(),
+        },
+      );
+    }
   }
 }
 
@@ -849,6 +953,40 @@ fn to_range(
   range
 }
 
+#[derive(Default)]
+struct ImportMetaResolveCollector {
+  specifiers: Vec<(SourceRange<SourcePos>, Atom)>,
+  diagnostic_ranges: Vec<SourceRange<SourcePos>>,
+}
+
+impl Visit for ImportMetaResolveCollector {
+  noop_visit_type!();
+
+  fn visit_call_expr(&mut self, node: &deno_ast::swc::ast::CallExpr) {
+    if node.args.len() == 1
+      && let Some(first_arg) = node.args.first()
+      && let Callee::Expr(callee) = &node.callee
+      && let Expr::Member(member) = &**callee
+      && let Expr::MetaProp(prop) = &*member.obj
+      && prop.kind == MetaPropKind::ImportMeta
+      && let MemberProp::Ident(ident) = &member.prop
+      && ident.sym == "resolve"
+      && first_arg.spread.is_none()
+    {
+      if let Expr::Lit(Lit::Str(arg)) = &*first_arg.expr {
+        let range = arg.range();
+        self.specifiers.push((
+          // remove quotes
+          SourceRange::new(range.start + 1, range.end - 1),
+          arg.value.clone(),
+        ));
+      } else {
+        self.diagnostic_ranges.push(first_arg.expr.range());
+      }
+    }
+  }
+}
+
 #[cfg(test)]
 mod tests {
   use std::sync::Arc;
@@ -958,6 +1096,9 @@ const test6 = await import(`./lib/something.ts`);
 // will warn
 const warn1 = await import(`lib${expr}`);
 const warn2 = await import(`${expr}`);
+
+import.meta.resolve("chalk");
+import.meta.resolve(nonAnalyzable);
 "#;
       let specifier =
         ModuleSpecifier::from_file_path(cwd.join("mod.ts")).unwrap();
@@ -966,7 +1107,7 @@ const warn2 = await import(`${expr}`);
       let mut reporter = |diagnostic| d.push(diagnostic);
       let unfurled_source =
         unfurl(&unfurler, &specifier, &source, &mut reporter);
-      assert_eq!(d.len(), 2);
+      assert_eq!(d.len(), 3);
       assert!(
         matches!(
           d[0],
@@ -983,6 +1124,14 @@ const warn2 = await import(`${expr}`);
         "{:?}",
         d[1]
       );
+      assert!(
+        matches!(
+          d[2],
+          SpecifierUnfurlerDiagnostic::UnanalyzableImportMetaResolve { .. }
+        ),
+        "{:?}",
+        d[2]
+      );
       let expected_source = r#"import express from "npm:express@5";"
 import foo from "./lib/foo.ts";
 import bar from "./lib/bar.ts";
@@ -1011,6 +1160,9 @@ const test6 = await import(`./lib/something.ts`);
 // will warn
 const warn1 = await import(`lib${expr}`);
 const warn2 = await import(`${expr}`);
+
+import.meta.resolve("npm:chalk@5");
+import.meta.resolve(nonAnalyzable);
 "#;
       assert_eq!(unfurled_source, expected_source);
     }