summaryrefslogtreecommitdiff
path: root/cli/tools/registry/graph.rs
blob: b64350887c21e135bbd90db4d00fa698abb2e047 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

use std::collections::HashSet;
use std::collections::VecDeque;

use deno_ast::ModuleSpecifier;
use deno_config::ConfigFile;
use deno_config::WorkspaceConfig;
use deno_core::anyhow::bail;
use deno_core::anyhow::Context;
use deno_core::error::AnyError;
use deno_graph::FastCheckDiagnostic;
use deno_graph::ModuleGraph;

#[derive(Debug)]
pub struct MemberRoots {
  pub name: String,
  pub dir_url: ModuleSpecifier,
  pub exports: Vec<ModuleSpecifier>,
}

pub fn get_workspace_member_roots(
  config: &WorkspaceConfig,
) -> Result<Vec<MemberRoots>, AnyError> {
  let mut members = Vec::with_capacity(config.members.len());
  let mut seen_names = HashSet::with_capacity(config.members.len());
  for member in &config.members {
    if !seen_names.insert(&member.package_name) {
      bail!(
        "Cannot have two workspace packages with the same name ('{}' at {})",
        member.package_name,
        member.path.display(),
      );
    }
    members.push(MemberRoots {
      name: member.package_name.clone(),
      dir_url: member.config_file.specifier.join("./").unwrap().clone(),
      exports: resolve_config_file_roots_from_exports(&member.config_file)?,
    });
  }
  Ok(members)
}

pub fn resolve_config_file_roots_from_exports(
  config_file: &ConfigFile,
) -> Result<Vec<ModuleSpecifier>, AnyError> {
  let exports_config = config_file
    .to_exports_config()
    .with_context(|| {
      format!("Failed to parse exports at {}", config_file.specifier)
    })?
    .into_map();
  let mut exports = Vec::with_capacity(exports_config.len());
  for (_, value) in exports_config {
    let entry_point =
      config_file.specifier.join(&value).with_context(|| {
        format!("Failed to join {} with {}", config_file.specifier, value)
      })?;
    exports.push(entry_point);
  }
  Ok(exports)
}

pub fn surface_fast_check_type_graph_errors(
  graph: &ModuleGraph,
  packages: &[MemberRoots],
) -> Result<(), AnyError> {
  let mut diagnostic_count = 0;
  let mut seen_diagnostics = HashSet::new();
  let mut seen_modules = HashSet::with_capacity(graph.specifiers_count());
  for package in packages {
    let mut pending = VecDeque::new();
    for export in &package.exports {
      if seen_modules.insert(export.clone()) {
        pending.push_back(export.clone());
      }
    }

    'analyze_package: while let Some(specifier) = pending.pop_front() {
      let Ok(Some(module)) = graph.try_get_prefer_types(&specifier) else {
        continue;
      };
      let Some(esm_module) = module.esm() else {
        continue;
      };
      if let Some(diagnostic) = esm_module.fast_check_diagnostic() {
        for diagnostic in diagnostic.flatten_multiple() {
          if matches!(
            diagnostic,
            FastCheckDiagnostic::UnsupportedJavaScriptEntrypoint { .. }
          ) {
            // ignore JS packages for fast check
            log::warn!(
              concat!(
                "{} Package '{}' is a JavaScript package without a corresponding ",
                "declaration file. This may lead to a non-optimal experience for ",
                "users of your package. For performance reasons, it's recommended ",
                "to ship a corresponding TypeScript declaration file or to ",
                "convert to TypeScript.",
              ),
              deno_runtime::colors::yellow("Warning"),
              package.name,
            );
            break 'analyze_package; // no need to keep analyzing this package
          } else {
            let message = diagnostic.message_with_range_for_test();
            if !seen_diagnostics.insert(message.clone()) {
              continue;
            }

            log::error!("\n{}", message);
            diagnostic_count += 1;
          }
        }
      }

      // analyze the next dependencies
      for dep in esm_module.dependencies_prefer_fast_check().values() {
        let Some(specifier) = graph.resolve_dependency_from_dep(dep, true)
        else {
          continue;
        };

        let dep_in_same_package =
          specifier.as_str().starts_with(package.dir_url.as_str());
        if dep_in_same_package {
          let is_new = seen_modules.insert(specifier.clone());
          if is_new {
            pending.push_back(specifier.clone());
          }
        }
      }
    }
  }

  if diagnostic_count > 0 {
    // for the time being, tell the user why we have these errors and the benefit they bring
    log::error!(
      concat!(
        "\nFixing these fast check errors is required to make the code fast check compatible ",
        "which enables type checking your package's TypeScript code with the same ",
        "performance as if you had distributed declaration files. Do any of these ",
        "errors seem too restrictive or incorrect? Please open an issue if so to ",
        "help us improve: https://github.com/denoland/deno/issues\n",
      )
    );
    bail!(
      "Had {} fast check error{}.",
      diagnostic_count,
      if diagnostic_count == 1 { "" } else { "s" }
    )
  }
  Ok(())
}