1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-01 16:51:13 -05:00
denoland-deno/cli/tools/vendor/import_map.rs
David Sherret 147411e64b
feat: npm workspace and better Deno workspace support (#24334)
Adds much better support for the unstable Deno workspaces as well as
support for npm workspaces. npm workspaces is still lacking in that we
only install packages into the root node_modules folder. We'll make it
smarter over time in order for it to figure out when to add node_modules
folders within packages.

This includes a breaking change in config file resolution where we stop
searching for config files on the first found package.json unless it's
in a workspace. For the previous behaviour, the root deno.json needs to
be updated to be a workspace by adding `"workspace":
["./path-to-pkg-json-folder-goes-here"]`. See details in
https://github.com/denoland/deno_config/pull/66

Closes #24340
Closes #24159
Closes #24161
Closes #22020
Closes #18546
Closes #16106
Closes #24160
2024-07-04 00:54:33 +00:00

508 lines
14 KiB
Rust

// Copyright 2018-2024 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::source::ResolutionMode;
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::args::JsxImportSourceConfig;
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,
maybe_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) = maybe_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 struct BuildImportMapInput<'a> {
pub base_dir: &'a ModuleSpecifier,
pub modules: &'a [&'a Module],
pub graph: &'a ModuleGraph,
pub mappings: &'a Mappings,
pub maybe_original_import_map: Option<&'a ImportMap>,
pub maybe_jsx_import_source: Option<&'a JsxImportSourceConfig>,
pub resolver: &'a dyn deno_graph::source::Resolver,
pub parsed_source_cache: &'a ParsedSourceCache,
}
pub fn build_import_map(
input: BuildImportMapInput<'_>,
) -> Result<String, AnyError> {
let BuildImportMapInput {
base_dir,
modules,
graph,
mappings,
maybe_original_import_map,
maybe_jsx_import_source,
resolver,
parsed_source_cache,
} = input;
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);
}
// add the jsx import source to the destination import map, if mapped in the original import map
if let Some(jsx_import_source) = maybe_jsx_import_source {
if let Some(specifier_text) = jsx_import_source.maybe_specifier_text() {
if let Ok(resolved_url) = resolver.resolve(
&specifier_text,
&deno_graph::Range {
specifier: jsx_import_source.base_url.clone(),
start: deno_graph::Position::zeroed(),
end: deno_graph::Position::zeroed(),
},
ResolutionMode::Execution,
) {
builder.imports.add(specifier_text, &resolved_url);
}
}
}
Ok(builder.into_import_map(maybe_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::Js(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_js_module(module)?;
let text_info = parsed_source.text_info_lazy().clone();
for dep in module.dependencies.values() {
visit_resolution(
&dep.maybe_code,
graph,
import_map,
&module.specifier,
mappings,
&text_info,
&module.source,
);
visit_resolution(
&dep.maybe_type,
graph,
import_map,
&module.specifier,
mappings,
&text_info,
&module.source,
);
}
if let Some(types_dep) = &module.maybe_types_dependency {
visit_resolution(
&types_dep.dependency,
graph,
import_map,
&module.specifier,
mappings,
&text_info,
&module.source,
);
}
}
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 if specifier.scheme() == "file" {
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()) {
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 {
// it's probably a redirect. Add it explicitly to the import map
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
}