1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-27 09:39:08 -05:00
denoland-deno/cli/tools/vendor/import_map.rs
David Sherret 890780a9e9
feat(unstable): ability to resolve specifiers with no extension, specifiers for a directory, and TS files from JS extensions (#21464)
Adds an `--unstable-sloppy-imports` flag which supports the
following for `file:` specifiers:

* Allows writing `./mod` in a specifier to do extension probing.
- ex. `import { Example } from "./example"` instead of `import { Example
} from "./example.ts"`
* Allows writing `./routes` to do directory extension probing for files
like `./routes/index.ts`
* Allows writing `./mod.js` for *mod.ts* files.

This functionality is **NOT RECOMMENDED** for general use with Deno:

1. It's not as optimal for perf:
https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-2/
1. It makes tooling in the ecosystem more complex in order to have to
understand this.
1. The "Deno way" is to be explicit about what you're doing. It's better
in the long run.
1. It doesn't work if published to the Deno registry because doing stuff
like extension probing with remote specifiers would be incredibly slow.

This is instead only recommended to help with migrating existing
projects to Deno. For example, it's very useful for getting CJS projects
written with import/export declaration working in Deno without modifying
module specifiers and for supporting TS ESM projects written with
`./mod.js` specifiers.

This feature will output warnings to guide the user towards correcting
their specifiers. Additionally, quick fixes are provided in the LSP to
update these specifiers:
2023-12-07 00:03:18 +00:00

509 lines
14 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::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,
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 struct BuildImportMapInput<'a> {
pub base_dir: &'a ModuleSpecifier,
pub modules: &'a [&'a Module],
pub graph: &'a ModuleGraph,
pub mappings: &'a Mappings,
pub original_import_map: Option<&'a ImportMap>,
pub 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,
original_import_map,
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) = 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(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 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
}