mirror of
https://github.com/denoland/deno.git
synced 2024-11-30 16:40:57 -05:00
147411e64b
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
508 lines
14 KiB
Rust
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
|
|
}
|