mirror of
https://github.com/denoland/deno.git
synced 2024-10-29 08:58:01 -04:00
a6ca4d0d61
This changes npm specifiers to be handled by deno_graph and resolved to an npm package name and version when the specifier is encountered. It also slightly changes how npm specifier resolution occurs—previously it would collect all the npm specifiers and resolve them all at once, but now it resolves them on the fly as they are encountered in the module graph. https://github.com/denoland/deno_graph/pull/232 --------- Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
473 lines
13 KiB
Rust
473 lines
13 KiB
Rust
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
|
|
|
use deno_ast::LineAndColumnIndex;
|
|
use deno_ast::ModuleSpecifier;
|
|
use deno_ast::SourceTextInfo;
|
|
use deno_core::error::AnyError;
|
|
use deno_graph::Module;
|
|
use deno_graph::ModuleGraph;
|
|
use deno_graph::Position;
|
|
use deno_graph::Range;
|
|
use deno_graph::Resolution;
|
|
use import_map::ImportMap;
|
|
use import_map::SpecifierMap;
|
|
use indexmap::IndexMap;
|
|
use log::warn;
|
|
|
|
use crate::cache::ParsedSourceCache;
|
|
|
|
use super::mappings::Mappings;
|
|
use super::specifiers::is_remote_specifier;
|
|
use super::specifiers::is_remote_specifier_text;
|
|
|
|
struct ImportMapBuilder<'a> {
|
|
base_dir: &'a ModuleSpecifier,
|
|
mappings: &'a Mappings,
|
|
imports: ImportsBuilder<'a>,
|
|
scopes: IndexMap<String, ImportsBuilder<'a>>,
|
|
}
|
|
|
|
impl<'a> ImportMapBuilder<'a> {
|
|
pub fn new(base_dir: &'a ModuleSpecifier, mappings: &'a Mappings) -> Self {
|
|
ImportMapBuilder {
|
|
base_dir,
|
|
mappings,
|
|
imports: ImportsBuilder::new(base_dir, mappings),
|
|
scopes: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub fn base_dir(&self) -> &ModuleSpecifier {
|
|
self.base_dir
|
|
}
|
|
|
|
pub fn scope(
|
|
&mut self,
|
|
base_specifier: &ModuleSpecifier,
|
|
) -> &mut ImportsBuilder<'a> {
|
|
self
|
|
.scopes
|
|
.entry(
|
|
self
|
|
.mappings
|
|
.relative_specifier_text(self.base_dir, base_specifier),
|
|
)
|
|
.or_insert_with(|| ImportsBuilder::new(self.base_dir, self.mappings))
|
|
}
|
|
|
|
pub fn into_import_map(
|
|
self,
|
|
original_import_map: Option<&ImportMap>,
|
|
) -> ImportMap {
|
|
fn get_local_imports(
|
|
new_relative_path: &str,
|
|
original_imports: &SpecifierMap,
|
|
) -> Vec<(String, String)> {
|
|
let mut result = Vec::new();
|
|
for entry in original_imports.entries() {
|
|
if let Some(raw_value) = entry.raw_value {
|
|
if raw_value.starts_with("./") || raw_value.starts_with("../") {
|
|
let sub_index = raw_value.find('/').unwrap() + 1;
|
|
result.push((
|
|
entry.raw_key.to_string(),
|
|
format!("{}{}", new_relative_path, &raw_value[sub_index..]),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
fn add_local_imports<'a>(
|
|
new_relative_path: &str,
|
|
original_imports: &SpecifierMap,
|
|
get_new_imports: impl FnOnce() -> &'a mut SpecifierMap,
|
|
) {
|
|
let local_imports =
|
|
get_local_imports(new_relative_path, original_imports);
|
|
if !local_imports.is_empty() {
|
|
let new_imports = get_new_imports();
|
|
for (key, value) in local_imports {
|
|
if let Err(warning) = new_imports.append(key, value) {
|
|
warn!("{}", warning);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut import_map = ImportMap::new(self.base_dir.clone());
|
|
|
|
if let Some(original_im) = original_import_map {
|
|
let original_base_dir = ModuleSpecifier::from_directory_path(
|
|
original_im
|
|
.base_url()
|
|
.to_file_path()
|
|
.unwrap()
|
|
.parent()
|
|
.unwrap(),
|
|
)
|
|
.unwrap();
|
|
let new_relative_path = self
|
|
.mappings
|
|
.relative_specifier_text(self.base_dir, &original_base_dir);
|
|
// add the imports
|
|
add_local_imports(&new_relative_path, original_im.imports(), || {
|
|
import_map.imports_mut()
|
|
});
|
|
|
|
for scope in original_im.scopes() {
|
|
if scope.raw_key.starts_with("./") || scope.raw_key.starts_with("../") {
|
|
let sub_index = scope.raw_key.find('/').unwrap() + 1;
|
|
let new_key =
|
|
format!("{}{}", new_relative_path, &scope.raw_key[sub_index..]);
|
|
add_local_imports(&new_relative_path, scope.imports, || {
|
|
import_map.get_or_append_scope_mut(&new_key).unwrap()
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
let imports = import_map.imports_mut();
|
|
for (key, value) in self.imports.imports {
|
|
if !imports.contains(&key) {
|
|
imports.append(key, value).unwrap();
|
|
}
|
|
}
|
|
|
|
for (scope_key, scope_value) in self.scopes {
|
|
if !scope_value.imports.is_empty() {
|
|
let imports = import_map.get_or_append_scope_mut(&scope_key).unwrap();
|
|
for (key, value) in scope_value.imports {
|
|
if !imports.contains(&key) {
|
|
imports.append(key, value).unwrap();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
import_map
|
|
}
|
|
}
|
|
|
|
struct ImportsBuilder<'a> {
|
|
base_dir: &'a ModuleSpecifier,
|
|
mappings: &'a Mappings,
|
|
imports: IndexMap<String, String>,
|
|
}
|
|
|
|
impl<'a> ImportsBuilder<'a> {
|
|
pub fn new(base_dir: &'a ModuleSpecifier, mappings: &'a Mappings) -> Self {
|
|
Self {
|
|
base_dir,
|
|
mappings,
|
|
imports: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub fn add(&mut self, key: String, specifier: &ModuleSpecifier) {
|
|
let value = self
|
|
.mappings
|
|
.relative_specifier_text(self.base_dir, specifier);
|
|
|
|
// skip creating identity entries
|
|
if key != value {
|
|
self.imports.insert(key, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn build_import_map(
|
|
base_dir: &ModuleSpecifier,
|
|
graph: &ModuleGraph,
|
|
modules: &[&Module],
|
|
mappings: &Mappings,
|
|
original_import_map: Option<&ImportMap>,
|
|
parsed_source_cache: &ParsedSourceCache,
|
|
) -> Result<String, AnyError> {
|
|
let mut builder = ImportMapBuilder::new(base_dir, mappings);
|
|
visit_modules(graph, modules, mappings, &mut builder, parsed_source_cache)?;
|
|
|
|
for base_specifier in mappings.base_specifiers() {
|
|
builder
|
|
.imports
|
|
.add(base_specifier.to_string(), base_specifier);
|
|
}
|
|
|
|
Ok(builder.into_import_map(original_import_map).to_json())
|
|
}
|
|
|
|
fn visit_modules(
|
|
graph: &ModuleGraph,
|
|
modules: &[&Module],
|
|
mappings: &Mappings,
|
|
import_map: &mut ImportMapBuilder,
|
|
parsed_source_cache: &ParsedSourceCache,
|
|
) -> Result<(), AnyError> {
|
|
for module in modules {
|
|
let module = match module {
|
|
Module::Esm(module) => module,
|
|
// skip visiting Json modules as they are leaves
|
|
Module::Json(_)
|
|
| Module::Npm(_)
|
|
| Module::Node(_)
|
|
| Module::External(_) => continue,
|
|
};
|
|
|
|
let parsed_source =
|
|
parsed_source_cache.get_parsed_source_from_esm_module(module)?;
|
|
let text_info = parsed_source.text_info().clone();
|
|
let source_text = &module.source;
|
|
|
|
for dep in module.dependencies.values() {
|
|
visit_resolution(
|
|
&dep.maybe_code,
|
|
graph,
|
|
import_map,
|
|
&module.specifier,
|
|
mappings,
|
|
&text_info,
|
|
source_text,
|
|
);
|
|
visit_resolution(
|
|
&dep.maybe_type,
|
|
graph,
|
|
import_map,
|
|
&module.specifier,
|
|
mappings,
|
|
&text_info,
|
|
source_text,
|
|
);
|
|
}
|
|
|
|
if let Some(types_dep) = &module.maybe_types_dependency {
|
|
visit_resolution(
|
|
&types_dep.dependency,
|
|
graph,
|
|
import_map,
|
|
&module.specifier,
|
|
mappings,
|
|
&text_info,
|
|
source_text,
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn visit_resolution(
|
|
resolution: &Resolution,
|
|
graph: &ModuleGraph,
|
|
import_map: &mut ImportMapBuilder,
|
|
referrer: &ModuleSpecifier,
|
|
mappings: &Mappings,
|
|
text_info: &SourceTextInfo,
|
|
source_text: &str,
|
|
) {
|
|
if let Some(resolved) = resolution.ok() {
|
|
let text = text_from_range(text_info, source_text, &resolved.range);
|
|
// if the text is empty then it's probably an x-TypeScript-types
|
|
if !text.is_empty() {
|
|
handle_dep_specifier(
|
|
text,
|
|
&resolved.specifier,
|
|
graph,
|
|
import_map,
|
|
referrer,
|
|
mappings,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_dep_specifier(
|
|
text: &str,
|
|
unresolved_specifier: &ModuleSpecifier,
|
|
graph: &ModuleGraph,
|
|
import_map: &mut ImportMapBuilder,
|
|
referrer: &ModuleSpecifier,
|
|
mappings: &Mappings,
|
|
) {
|
|
let specifier = match graph.get(unresolved_specifier) {
|
|
Some(module) => module.specifier().clone(),
|
|
// Ignore when None. The graph was previous validated so this is a
|
|
// dynamic import that was missing and is ignored for vendoring
|
|
None => return,
|
|
};
|
|
// check if it's referencing a remote module
|
|
if is_remote_specifier(&specifier) {
|
|
handle_remote_dep_specifier(
|
|
text,
|
|
unresolved_specifier,
|
|
&specifier,
|
|
import_map,
|
|
referrer,
|
|
mappings,
|
|
)
|
|
} else {
|
|
handle_local_dep_specifier(
|
|
text,
|
|
unresolved_specifier,
|
|
&specifier,
|
|
import_map,
|
|
referrer,
|
|
mappings,
|
|
);
|
|
}
|
|
}
|
|
|
|
fn handle_remote_dep_specifier(
|
|
text: &str,
|
|
unresolved_specifier: &ModuleSpecifier,
|
|
specifier: &ModuleSpecifier,
|
|
import_map: &mut ImportMapBuilder,
|
|
referrer: &ModuleSpecifier,
|
|
mappings: &Mappings,
|
|
) {
|
|
if is_remote_specifier_text(text) {
|
|
let base_specifier = mappings.base_specifier(specifier);
|
|
if !text.starts_with(base_specifier.as_str()) {
|
|
panic!("Expected {text} to start with {base_specifier}");
|
|
}
|
|
|
|
let sub_path = &text[base_specifier.as_str().len()..];
|
|
let relative_text =
|
|
mappings.relative_specifier_text(base_specifier, specifier);
|
|
let expected_sub_path = relative_text.trim_start_matches("./");
|
|
if expected_sub_path != sub_path {
|
|
import_map.imports.add(text.to_string(), specifier);
|
|
}
|
|
} else {
|
|
let expected_relative_specifier_text =
|
|
mappings.relative_specifier_text(referrer, specifier);
|
|
if expected_relative_specifier_text == text {
|
|
return;
|
|
}
|
|
|
|
if !is_remote_specifier(referrer) {
|
|
// local module referencing a remote module using
|
|
// non-remote specifier text means it was something in
|
|
// the original import map, so add a mapping to it
|
|
import_map.imports.add(text.to_string(), specifier);
|
|
return;
|
|
}
|
|
|
|
let base_referrer = mappings.base_specifier(referrer);
|
|
let base_dir = import_map.base_dir().clone();
|
|
let imports = import_map.scope(base_referrer);
|
|
if text.starts_with("./") || text.starts_with("../") {
|
|
// resolve relative specifier key
|
|
let mut local_base_specifier = mappings.local_uri(base_referrer);
|
|
local_base_specifier = local_base_specifier
|
|
// path includes "/" so make it relative
|
|
.join(&format!(".{}", unresolved_specifier.path()))
|
|
.unwrap_or_else(|_| {
|
|
panic!(
|
|
"Error joining {} to {}",
|
|
unresolved_specifier.path(),
|
|
local_base_specifier
|
|
)
|
|
});
|
|
local_base_specifier.set_query(unresolved_specifier.query());
|
|
|
|
imports.add(
|
|
mappings.relative_specifier_text(&base_dir, &local_base_specifier),
|
|
specifier,
|
|
);
|
|
|
|
// add a mapping that uses the local directory name and the remote
|
|
// filename in order to support files importing this relatively
|
|
imports.add(
|
|
{
|
|
let local_path = mappings.local_path(specifier);
|
|
let mut value =
|
|
ModuleSpecifier::from_directory_path(local_path.parent().unwrap())
|
|
.unwrap();
|
|
value.set_query(specifier.query());
|
|
value.set_path(&format!(
|
|
"{}{}",
|
|
value.path(),
|
|
specifier.path_segments().unwrap().last().unwrap(),
|
|
));
|
|
mappings.relative_specifier_text(&base_dir, &value)
|
|
},
|
|
specifier,
|
|
);
|
|
} else {
|
|
// absolute (`/`) or bare specifier should be left as-is
|
|
imports.add(text.to_string(), specifier);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_local_dep_specifier(
|
|
text: &str,
|
|
unresolved_specifier: &ModuleSpecifier,
|
|
specifier: &ModuleSpecifier,
|
|
import_map: &mut ImportMapBuilder,
|
|
referrer: &ModuleSpecifier,
|
|
mappings: &Mappings,
|
|
) {
|
|
if !is_remote_specifier(referrer) {
|
|
// do not handle local modules referencing local modules
|
|
return;
|
|
}
|
|
|
|
// The remote module is referencing a local file. This could occur via an
|
|
// existing import map. In this case, we'll have to add an import map
|
|
// entry in order to map the path back to the local path once vendored.
|
|
let base_dir = import_map.base_dir().clone();
|
|
let base_specifier = mappings.base_specifier(referrer);
|
|
let imports = import_map.scope(base_specifier);
|
|
|
|
if text.starts_with("./") || text.starts_with("../") {
|
|
let referrer_local_uri = mappings.local_uri(referrer);
|
|
let mut specifier_local_uri =
|
|
referrer_local_uri.join(text).unwrap_or_else(|_| {
|
|
panic!(
|
|
"Error joining {} to {}",
|
|
unresolved_specifier.path(),
|
|
referrer_local_uri
|
|
)
|
|
});
|
|
specifier_local_uri.set_query(unresolved_specifier.query());
|
|
|
|
imports.add(
|
|
mappings.relative_specifier_text(&base_dir, &specifier_local_uri),
|
|
specifier,
|
|
);
|
|
} else {
|
|
imports.add(text.to_string(), specifier);
|
|
}
|
|
}
|
|
|
|
fn text_from_range<'a>(
|
|
text_info: &SourceTextInfo,
|
|
text: &'a str,
|
|
range: &Range,
|
|
) -> &'a str {
|
|
let result = &text[byte_range(text_info, range)];
|
|
if result.starts_with('"') || result.starts_with('\'') {
|
|
// remove the quotes
|
|
&result[1..result.len() - 1]
|
|
} else {
|
|
result
|
|
}
|
|
}
|
|
|
|
fn byte_range(
|
|
text_info: &SourceTextInfo,
|
|
range: &Range,
|
|
) -> std::ops::Range<usize> {
|
|
let start = byte_index(text_info, &range.start);
|
|
let end = byte_index(text_info, &range.end);
|
|
start..end
|
|
}
|
|
|
|
fn byte_index(text_info: &SourceTextInfo, pos: &Position) -> usize {
|
|
// todo(https://github.com/denoland/deno_graph/issues/79): use byte indexes all the way down
|
|
text_info.loc_to_source_pos(LineAndColumnIndex {
|
|
line_index: pos.line,
|
|
column_index: pos.character,
|
|
}) - text_info.range().start
|
|
}
|