mirror of
https://github.com/denoland/deno.git
synced 2024-12-22 15:24:46 -05:00
890780a9e9
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:
509 lines
14 KiB
Rust
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
|
|
}
|