use crate::analyzer::analyze_deno_types;
use crate::analyzer::DependencyKind;
use crate::analyzer::ModuleAnalyzer;
use crate::analyzer::ModuleInfo;
use crate::analyzer::PositionRange;
use crate::analyzer::SpecifierWithRange;
use crate::analyzer::TypeScriptReference;
use crate::module_specifier::resolve_import;
use crate::module_specifier::ModuleSpecifier;
use crate::module_specifier::SpecifierError;
use crate::source::*;
use anyhow::Error;
use anyhow::Result;
use deno_ast::LineAndColumnIndex;
use deno_ast::MediaType;
use deno_ast::SourcePos;
use deno_ast::SourceTextInfo;
use futures::stream::FuturesUnordered;
use futures::stream::StreamExt;
use futures::Future;
use futures::FutureExt;
use serde::ser::SerializeMap;
use serde::ser::SerializeSeq;
use serde::ser::SerializeStruct;
use serde::Deserialize;
use serde::Serialize;
use serde::Serializer;
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fmt;
use std::pin::Pin;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Position {
pub line: usize,
pub character: usize,
}
impl PartialOrd for Position {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Position {
fn cmp(&self, other: &Self) -> Ordering {
match self.line.cmp(&other.line) {
Ordering::Equal => self.character.cmp(&other.character),
Ordering::Greater => Ordering::Greater,
Ordering::Less => Ordering::Less,
}
}
}
impl Position {
pub fn zeroed() -> Self {
Self {
line: 0,
character: 0,
}
}
pub fn from_source_pos(pos: SourcePos, text_info: &SourceTextInfo) -> Self {
let line_and_column_index = text_info.line_and_column_index(pos);
Self {
line: line_and_column_index.line_index,
character: line_and_column_index.column_index,
}
}
pub fn as_source_pos(&self, text_info: &SourceTextInfo) -> SourcePos {
text_info.loc_to_source_pos(LineAndColumnIndex {
line_index: self.line,
column_index: self.character,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Range {
#[serde(skip_serializing)]
pub specifier: ModuleSpecifier,
#[serde(default = "Position::zeroed")]
pub start: Position,
#[serde(default = "Position::zeroed")]
pub end: Position,
}
impl fmt::Display for Range {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}:{}:{}",
self.specifier,
self.start.line + 1,
self.start.character + 1
)
}
}
impl Range {
pub(crate) fn from_position_range(
specifier: &ModuleSpecifier,
range: &PositionRange,
) -> Range {
Range {
specifier: specifier.clone(),
start: range.start.clone(),
end: range.end.clone(),
}
}
pub fn includes(&self, position: &Position) -> bool {
(position >= &self.start) && (position <= &self.end)
}
}
#[derive(Debug)]
pub enum ModuleGraphError {
InvalidTypeAssertion {
specifier: ModuleSpecifier,
actual_media_type: MediaType,
expected_media_type: MediaType,
},
LoadingErr(ModuleSpecifier, Arc<anyhow::Error>),
Missing(ModuleSpecifier),
ParseErr(ModuleSpecifier, deno_ast::Diagnostic),
ResolutionError(ResolutionError),
UnsupportedImportAssertionType(ModuleSpecifier, String),
UnsupportedMediaType(ModuleSpecifier, MediaType),
}
impl Clone for ModuleGraphError {
fn clone(&self) -> Self {
match self {
Self::LoadingErr(specifier, err) => {
Self::LoadingErr(specifier.clone(), err.clone())
}
Self::ParseErr(specifier, err) => {
Self::ParseErr(specifier.clone(), err.clone())
}
Self::ResolutionError(err) => Self::ResolutionError(err.clone()),
Self::InvalidTypeAssertion {
specifier,
actual_media_type,
expected_media_type,
} => Self::InvalidTypeAssertion {
specifier: specifier.clone(),
actual_media_type: *actual_media_type,
expected_media_type: *expected_media_type,
},
Self::UnsupportedImportAssertionType(specifier, kind) => {
Self::UnsupportedImportAssertionType(specifier.clone(), kind.clone())
}
Self::UnsupportedMediaType(specifier, media_type) => {
Self::UnsupportedMediaType(specifier.clone(), *media_type)
}
Self::Missing(specifier) => Self::Missing(specifier.clone()),
}
}
}
impl ModuleGraphError {
#[cfg(feature = "rust")]
pub fn specifier(&self) -> &ModuleSpecifier {
match self {
Self::ResolutionError(err) => &err.range().specifier,
Self::LoadingErr(s, _)
| Self::ParseErr(s, _)
| Self::UnsupportedMediaType(s, _)
| Self::UnsupportedImportAssertionType(s, _)
| Self::Missing(s) => s,
Self::InvalidTypeAssertion { specifier, .. } => specifier,
}
}
}
impl std::error::Error for ModuleGraphError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::LoadingErr(_, err) => Some(err.as_ref().as_ref()),
Self::ResolutionError(ref err) => Some(err),
Self::InvalidTypeAssertion { .. }
| Self::Missing(_)
| Self::ParseErr(_, _)
| Self::UnsupportedImportAssertionType(_, _)
| Self::UnsupportedMediaType(_, _) => None,
}
}
}
impl fmt::Display for ModuleGraphError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::LoadingErr(_, err) => err.fmt(f),
Self::ParseErr(_, diagnostic) => write!(f, "The module's source code could not be parsed: {}", diagnostic),
Self::ResolutionError(err) => err.fmt(f),
Self::InvalidTypeAssertion { specifier, actual_media_type, expected_media_type } => write!(f, "Expected a {} module, but identified a {} module.\n Specifier: {}", expected_media_type, actual_media_type, specifier),
Self::UnsupportedMediaType(specifier, MediaType::Json) => write!(f, "Expected a JavaScript or TypeScript module, but identified a Json module. Consider importing Json modules with an import assertion with the type of \"json\".\n Specifier: {}", specifier),
Self::UnsupportedMediaType(specifier, media_type) => write!(f, "Expected a JavaScript or TypeScript module, but identified a {} module. Importing these types of modules is currently not supported.\n Specifier: {}", media_type, specifier),
Self::UnsupportedImportAssertionType(_, kind) => write!(f, "The import assertion type of \"{}\" is unsupported.", kind),
Self::Missing(specifier) => write!(f, "Module not found \"{}\".", specifier),
}
}
}
impl<'a> From<&'a ResolutionError> for ModuleGraphError {
fn from(err: &'a ResolutionError) -> Self {
Self::ResolutionError(err.clone())
}
}
#[derive(Debug, Clone)]
pub enum ResolutionError {
InvalidDowngrade {
specifier: ModuleSpecifier,
range: Range,
},
InvalidLocalImport {
specifier: ModuleSpecifier,
range: Range,
},
InvalidSpecifier {
error: SpecifierError,
range: Range,
},
ResolverError {
error: Arc<anyhow::Error>,
specifier: String,
range: Range,
},
}
impl ResolutionError {
pub fn range(&self) -> &Range {
match self {
Self::InvalidDowngrade { range, .. }
| Self::InvalidLocalImport { range, .. }
| Self::InvalidSpecifier { range, .. }
| Self::ResolverError { range, .. } => range,
}
}
#[cfg(feature = "rust")]
pub fn to_string_with_range(&self) -> String {
format!("{}\n at {}", self, self.range())
}
}
impl std::error::Error for ResolutionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidDowngrade { .. } | Self::InvalidLocalImport { .. } => None,
Self::InvalidSpecifier { ref error, .. } => Some(error),
Self::ResolverError { error, .. } => Some(error.as_ref().as_ref()),
}
}
}
impl PartialEq for ResolutionError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(
Self::ResolverError {
specifier: a,
range: a_range,
..
},
Self::ResolverError {
specifier: b,
range: b_range,
..
},
) => a == b && a_range == b_range,
(
Self::InvalidDowngrade {
specifier: a,
range: a_range,
..
},
Self::InvalidDowngrade {
specifier: b,
range: b_range,
..
},
)
| (
Self::InvalidLocalImport {
specifier: a,
range: a_range,
..
},
Self::InvalidLocalImport {
specifier: b,
range: b_range,
..
},
) => a == b && a_range == b_range,
(
Self::InvalidSpecifier {
error: a,
range: a_range,
..
},
Self::InvalidSpecifier {
error: b,
range: b_range,
..
},
) => a == b && a_range == b_range,
_ => false,
}
}
}
impl Eq for ResolutionError {}
impl fmt::Display for ResolutionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidDowngrade { specifier, .. } => write!(f, "Modules imported via https are not allowed to import http modules.\n Importing: {}", specifier),
Self::InvalidLocalImport { specifier, .. } => write!(f, "Remote modules are not allowed to import local modules. Consider using a dynamic import instead.\n Importing: {}", specifier),
Self::ResolverError { error, .. } => error.fmt(f),
Self::InvalidSpecifier { error, .. } => error.fmt(f),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Resolved {
None,
Ok {
specifier: ModuleSpecifier,
range: Box<Range>,
},
Err(ResolutionError),
}
impl Resolved {
pub fn from_resolve_result(
result: Result<ModuleSpecifier, Error>,
range: Range,
specifier: &str,
remapped: bool,
) -> Self {
match result {
Ok(specifier) => Self::from_specifier(specifier, range, remapped),
Err(err) => {
let resolution_error =
if let Some(specifier_error) = err.downcast_ref::<SpecifierError>() {
ResolutionError::InvalidSpecifier {
error: specifier_error.clone(),
range,
}
} else {
ResolutionError::ResolverError {
error: Arc::new(err),
specifier: specifier.to_string(),
range,
}
};
Self::Err(resolution_error)
}
}
}
fn from_specifier(
specifier: ModuleSpecifier,
range: Range,
remapped: bool,
) -> Self {
let referrer_scheme = range.specifier.scheme();
let specifier_scheme = specifier.scheme();
if referrer_scheme == "https" && specifier_scheme == "http" {
Resolved::Err(ResolutionError::InvalidDowngrade { specifier, range })
} else if matches!(referrer_scheme, "https" | "http")
&& !matches!(specifier_scheme, "https" | "http" | "npm" | "node")
&& !remapped
{
Resolved::Err(ResolutionError::InvalidLocalImport { specifier, range })
} else {
Resolved::Ok {
specifier,
range: Box::new(range),
}
}
}
#[cfg(feature = "rust")]
pub fn includes(&self, position: &Position) -> Option<&Range> {
match self {
Self::Ok { range, .. } if range.includes(position) => Some(range),
Self::Err(err) => {
let range = err.range();
if range.includes(position) {
Some(range)
} else {
None
}
}
_ => None,
}
}
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
pub fn maybe_specifier(&self) -> Option<&ModuleSpecifier> {
if let Self::Ok { specifier, .. } = self {
Some(specifier)
} else {
None
}
}
}
impl Default for Resolved {
fn default() -> Self {
Self::None
}
}
fn serialize_resolved<S>(
resolved: &Resolved,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match resolved {
Resolved::Ok {
specifier, range, ..
} => {
let mut state = serializer.serialize_struct("ResolvedSpecifier", 2)?;
state.serialize_field("specifier", specifier)?;
state.serialize_field("span", range)?;
state.end()
}
Resolved::Err(err) => {
let mut state = serializer.serialize_struct("ResolvedError", 2)?;
state.serialize_field("error", &err.to_string())?;
state.serialize_field("span", err.range())?;
state.end()
}
_ => Serialize::serialize(&serde_json::Value::Null, serializer),
}
}
fn is_false(v: &bool) -> bool {
!v
}
#[derive(Debug, Default, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Dependency {
#[serde(
rename = "code",
skip_serializing_if = "Resolved::is_none",
serialize_with = "serialize_resolved"
)]
pub maybe_code: Resolved,
#[serde(
rename = "type",
skip_serializing_if = "Resolved::is_none",
serialize_with = "serialize_resolved"
)]
pub maybe_type: Resolved,
#[serde(skip_serializing_if = "is_false")]
pub is_dynamic: bool,
#[serde(rename = "assertType", skip_serializing_if = "Option::is_none")]
pub maybe_assert_type: Option<String>,
}
#[cfg(feature = "rust")]
impl Dependency {
pub fn get_code(&self) -> Option<&ModuleSpecifier> {
self.maybe_code.maybe_specifier()
}
pub fn get_type(&self) -> Option<&ModuleSpecifier> {
self.maybe_type.maybe_specifier()
}
pub fn includes(&self, position: &Position) -> Option<&Range> {
let code = self.maybe_code.includes(position);
if code.is_none() {
self.maybe_type.includes(position)
} else {
code
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum ModuleKind {
Asserted,
Esm,
External,
Script,
}
fn is_media_type_unknown(media_type: &MediaType) -> bool {
matches!(media_type, MediaType::Unknown)
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Module {
#[serde(
skip_serializing_if = "BTreeMap::is_empty",
serialize_with = "serialize_dependencies"
)]
pub dependencies: BTreeMap<String, Dependency>,
pub kind: ModuleKind,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub maybe_cache_info: Option<CacheInfo>,
#[serde(
rename = "size",
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_maybe_source"
)]
pub maybe_source: Option<Arc<str>>,
#[serde(
rename = "typesDependency",
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_type_dependency"
)]
pub maybe_types_dependency: Option<(String, Resolved)>,
#[serde(skip_serializing_if = "is_media_type_unknown")]
pub media_type: MediaType,
pub specifier: ModuleSpecifier,
}
impl Module {
fn new(
specifier: ModuleSpecifier,
kind: ModuleKind,
source: Arc<str>,
) -> Self {
Self {
dependencies: Default::default(),
kind,
maybe_cache_info: None,
maybe_source: Some(source),
maybe_types_dependency: None,
media_type: MediaType::Unknown,
specifier,
}
}
pub fn new_without_source(
specifier: ModuleSpecifier,
kind: ModuleKind,
) -> Self {
Self {
dependencies: Default::default(),
kind,
maybe_cache_info: None,
maybe_source: None,
maybe_types_dependency: None,
media_type: MediaType::Unknown,
specifier,
}
}
pub fn size(&self) -> usize {
self
.maybe_source
.as_ref()
.map(|s| s.as_bytes().len())
.unwrap_or(0)
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub(crate) enum ModuleSlot {
Module(Module),
Err(ModuleGraphError),
Pending,
}
#[cfg(feature = "rust")]
type ModuleResult<'a> = (
&'a ModuleSpecifier,
Result<(&'a ModuleSpecifier, ModuleKind, MediaType), &'a ModuleGraphError>,
);
#[cfg(feature = "rust")]
fn to_result<'a>(
(specifier, module_slot): (&'a ModuleSpecifier, &'a ModuleSlot),
) -> Option<ModuleResult<'a>> {
match module_slot {
ModuleSlot::Err(err) => Some((specifier, Err(err))),
ModuleSlot::Module(module) => Some((
specifier,
Ok((&module.specifier, module.kind, module.media_type)),
)),
ModuleSlot::Pending => None,
}
}
#[derive(Debug, Clone, Serialize)]
pub struct GraphImport {
pub referrer: ModuleSpecifier,
#[serde(serialize_with = "serialize_dependencies")]
pub dependencies: BTreeMap<String, Dependency>,
}
impl GraphImport {
pub fn new(
referrer: ModuleSpecifier,
imports: Vec<String>,
maybe_resolver: Option<&dyn Resolver>,
) -> Self {
let dependencies = imports
.iter()
.map(|import| {
let referrer_range = Range {
specifier: referrer.clone(),
start: Position::zeroed(),
end: Position::zeroed(),
};
let maybe_type = resolve(import, &referrer_range, maybe_resolver);
(
import.clone(),
Dependency {
is_dynamic: false,
maybe_code: Resolved::None,
maybe_type,
maybe_assert_type: None,
},
)
})
.collect();
Self {
referrer,
dependencies,
}
}
}
#[derive(Debug, Serialize)]
pub struct ModuleGraph {
pub roots: Vec<ModuleSpecifier>,
#[serde(serialize_with = "serialize_modules", rename = "modules")]
pub(crate) module_slots: BTreeMap<ModuleSpecifier, ModuleSlot>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub imports: Vec<GraphImport>,
pub redirects: BTreeMap<ModuleSpecifier, ModuleSpecifier>,
}
impl ModuleGraph {
fn new(roots: Vec<ModuleSpecifier>) -> Self {
Self {
roots,
module_slots: Default::default(),
imports: Default::default(),
redirects: Default::default(),
}
}
#[cfg(feature = "rust")]
pub fn contains(&self, specifier: &ModuleSpecifier) -> bool {
let specifier = self.resolve(specifier);
self
.module_slots
.get(&specifier)
.map_or(false, |ms| matches!(ms, ModuleSlot::Module(_)))
}
#[cfg(feature = "rust")]
pub fn errors(&self) -> impl Iterator<Item = &ModuleGraphError> {
self.module_slots.values().filter_map(|ms| match ms {
ModuleSlot::Err(err) => Some(err),
ModuleSlot::Module(_) | ModuleSlot::Pending => None,
})
}
#[cfg(feature = "rust")]
pub fn get(&self, specifier: &ModuleSpecifier) -> Option<&Module> {
let specifier = self.resolve(specifier);
match self.module_slots.get(&specifier) {
Some(ModuleSlot::Module(module)) => Some(module),
_ => None,
}
}
#[cfg(feature = "rust")]
pub fn modules(&self) -> impl Iterator<Item = &Module> {
self.module_slots.values().filter_map(|ms| match ms {
ModuleSlot::Module(m) => Some(m),
_ => None,
})
}
pub fn resolve(&self, specifier: &ModuleSpecifier) -> ModuleSpecifier {
let mut redirected_specifier = specifier;
let mut seen = HashSet::new();
seen.insert(redirected_specifier);
while let Some(specifier) = self.redirects.get(redirected_specifier) {
if !seen.insert(specifier) {
eprintln!("An infinite loop of redirections detected.\n Original specifier: {}", specifier);
break;
}
redirected_specifier = specifier;
if seen.len() > 5 {
eprintln!("An excessive number of redirections detected.\n Original specifier: {}", specifier);
break;
}
}
redirected_specifier.clone()
}
pub fn resolve_dependency(
&self,
specifier: &str,
referrer: &ModuleSpecifier,
prefer_types: bool,
) -> Option<&ModuleSpecifier> {
let referrer = self.resolve(referrer);
let specifier = if let Some(ModuleSlot::Module(referring_module)) =
self.module_slots.get(&referrer)
{
let dependency = referring_module.dependencies.get(specifier)?;
self.resolve_dependency_specifier(dependency, prefer_types)
} else if let Some(graph_import) =
self.imports.iter().find(|i| i.referrer == referrer)
{
let dependency = graph_import.dependencies.get(specifier)?;
self.resolve_dependency_specifier(dependency, prefer_types)
} else {
None
}?;
match self.module_slots.get(&self.resolve(specifier)) {
Some(ModuleSlot::Module(m)) => Some(&m.specifier),
_ => None,
}
}
fn resolve_dependency_specifier<'a>(
&'a self,
dependency: &'a Dependency,
prefer_types: bool,
) -> Option<&'a ModuleSpecifier> {
let (maybe_first, maybe_second) = if prefer_types {
(&dependency.maybe_type, &dependency.maybe_code)
} else {
(&dependency.maybe_code, &dependency.maybe_type)
};
let specifier = maybe_first
.maybe_specifier()
.or_else(|| maybe_second.maybe_specifier());
match specifier {
Some(specifier) => {
if prefer_types {
return Some(
self
.resolve_types_dependency(specifier)
.unwrap_or(specifier),
);
}
Some(specifier)
}
None => None,
}
}
fn resolve_types_dependency(
&self,
specifier: &ModuleSpecifier,
) -> Option<&ModuleSpecifier> {
if let Some(ModuleSlot::Module(module)) = self.module_slots.get(specifier) {
if let Some((_, Resolved::Ok { specifier, .. })) =
&module.maybe_types_dependency
{
return Some(specifier);
}
}
None
}
#[cfg(feature = "rust")]
pub fn specifiers(
&self,
) -> impl Iterator<
Item = (
&ModuleSpecifier,
Result<(&ModuleSpecifier, ModuleKind, MediaType), &ModuleGraphError>,
),
> {
self.module_slots.iter().filter_map(to_result).chain(
self.redirects.iter().filter_map(|(specifier, found)| {
let module_slot = self.module_slots.get(found)?;
to_result((specifier, module_slot))
}),
)
}
pub fn try_get(
&self,
specifier: &ModuleSpecifier,
) -> Result<Option<&Module>, ModuleGraphError> {
let specifier = self.resolve(specifier);
match self.module_slots.get(&specifier) {
Some(ModuleSlot::Module(module)) => Ok(Some(module)),
Some(ModuleSlot::Err(err)) => Err(err.clone()),
_ => Ok(None),
}
}
#[cfg(feature = "rust")]
pub fn valid(&self) -> Result<(), ModuleGraphError> {
self.validate(false)
}
#[cfg(feature = "rust")]
pub fn valid_types_only(&self) -> Result<(), ModuleGraphError> {
self.validate(true)
}
#[cfg(feature = "rust")]
fn validate(&self, types_only: bool) -> Result<(), ModuleGraphError> {
fn validate<F>(
specifier: &ModuleSpecifier,
types_only: bool,
is_type: bool,
seen: &mut HashSet<ModuleSpecifier>,
get_module: &F,
) -> Result<(), ModuleGraphError>
where
F: Fn(&ModuleSpecifier) -> Result<Option<Module>, ModuleGraphError>,
{
if seen.contains(specifier) {
return Ok(());
}
seen.insert(specifier.clone());
let should_error = (is_type && types_only) || (!is_type && !types_only);
match get_module(specifier) {
Ok(Some(module)) => {
if let Some((_, Resolved::Ok { specifier, .. })) =
&module.maybe_types_dependency
{
validate(specifier, types_only, true, seen, get_module)?;
}
for dep in module.dependencies.values() {
if !dep.is_dynamic {
match &dep.maybe_type {
Resolved::Ok { specifier, .. } => {
validate(specifier, types_only, true, seen, get_module)?
}
Resolved::Err(err) if types_only => {
return Err(err.into());
}
_ => (),
}
match &dep.maybe_code {
Resolved::Ok { specifier, .. } => {
validate(specifier, types_only, false, seen, get_module)?
}
Resolved::Err(err) if !types_only => {
return Err(err.into());
}
_ => (),
}
}
}
Ok(())
}
Ok(None) if should_error => {
Err(ModuleGraphError::Missing(specifier.clone()))
}
Err(err) if should_error => Err(err),
_ => Ok(()),
}
}
let mut seen = HashSet::new();
for root in &self.roots {
validate(root, types_only, false, &mut seen, &|s| {
self.try_get(s).map(|o| o.cloned())
})?;
}
Ok(())
}
}
fn resolve(
specifier: &str,
referrer_range: &Range,
maybe_resolver: Option<&dyn Resolver>,
) -> Resolved {
if let Some(resolver) = maybe_resolver {
let response = resolver.resolve(specifier, &referrer_range.specifier);
Resolved::from_resolve_result(
response,
referrer_range.clone(),
specifier,
true,
)
} else {
let response = resolve_import(specifier, &referrer_range.specifier)
.map_err(|err| err.into());
Resolved::from_resolve_result(
response,
referrer_range.clone(),
specifier,
false,
)
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn parse_module(
specifier: &ModuleSpecifier,
maybe_headers: Option<&HashMap<String, String>>,
content: Arc<str>,
maybe_assert_type: Option<&str>,
maybe_kind: Option<ModuleKind>,
maybe_resolver: Option<&dyn Resolver>,
module_analyzer: &dyn ModuleAnalyzer,
is_root: bool,
is_dynamic_branch: bool,
) -> ModuleSlot {
let media_type =
MediaType::from_specifier_and_headers(specifier, maybe_headers);
if media_type == MediaType::Json
&& (is_root
|| is_dynamic_branch
|| matches!(maybe_assert_type, Some("json")))
{
return ModuleSlot::Module(Module {
dependencies: Default::default(),
kind: ModuleKind::Asserted,
maybe_cache_info: None,
maybe_source: Some(content),
maybe_types_dependency: None,
media_type: MediaType::Json,
specifier: specifier.clone(),
});
}
match maybe_assert_type {
Some("json") => {
return ModuleSlot::Err(ModuleGraphError::InvalidTypeAssertion {
specifier: specifier.clone(),
actual_media_type: media_type,
expected_media_type: MediaType::Json,
})
}
Some(assert_type) => {
return ModuleSlot::Err(ModuleGraphError::UnsupportedImportAssertionType(
specifier.clone(),
assert_type.to_string(),
))
}
_ => (),
}
match &media_type {
MediaType::JavaScript
| MediaType::Mjs
| MediaType::Cjs
| MediaType::Jsx
| MediaType::TypeScript
| MediaType::Mts
| MediaType::Cts
| MediaType::Tsx
| MediaType::Dts
| MediaType::Dmts
| MediaType::Dcts => {
match module_analyzer.analyze(specifier, content.clone(), media_type) {
Ok(module_info) => {
ModuleSlot::Module(parse_module_from_module_info(
specifier,
maybe_kind.unwrap_or(ModuleKind::Esm),
media_type,
maybe_headers,
module_info,
content,
maybe_resolver,
))
}
Err(diagnostic) => ModuleSlot::Err(ModuleGraphError::ParseErr(
specifier.clone(),
diagnostic,
)),
}
}
MediaType::Unknown if is_root => {
match module_analyzer.analyze(
specifier,
content.clone(),
MediaType::JavaScript,
) {
Ok(module_info) => {
ModuleSlot::Module(parse_module_from_module_info(
specifier,
maybe_kind.unwrap_or(ModuleKind::Esm),
media_type,
maybe_headers,
module_info,
content,
maybe_resolver,
))
}
Err(diagnostic) => ModuleSlot::Err(ModuleGraphError::ParseErr(
specifier.clone(),
diagnostic,
)),
}
}
_ => ModuleSlot::Err(ModuleGraphError::UnsupportedMediaType(
specifier.clone(),
media_type,
)),
}
}
pub(crate) fn parse_module_from_module_info(
specifier: &ModuleSpecifier,
kind: ModuleKind,
media_type: MediaType,
maybe_headers: Option<&HashMap<String, String>>,
module_info: ModuleInfo,
source: Arc<str>,
maybe_resolver: Option<&dyn Resolver>,
) -> Module {
let mut module = Module::new(specifier.clone(), kind, source);
module.media_type = media_type;
for reference in module_info.ts_references {
match reference {
TypeScriptReference::Path(specifier) => {
let range =
Range::from_position_range(&module.specifier, &specifier.range);
let resolved_dependency =
resolve(&specifier.text, &range, maybe_resolver);
let dep = module.dependencies.entry(specifier.text).or_default();
dep.maybe_code = resolved_dependency;
}
TypeScriptReference::Types(specifier) => {
let range =
Range::from_position_range(&module.specifier, &specifier.range);
let resolved_dependency =
resolve(&specifier.text, &range, maybe_resolver);
if is_untyped(&module.media_type) {
module.maybe_types_dependency =
Some((specifier.text, resolved_dependency));
} else {
let dep = module.dependencies.entry(specifier.text).or_default();
dep.maybe_type = resolved_dependency;
}
}
}
}
if matches!(media_type, MediaType::Jsx | MediaType::Tsx) {
let res = module_info.jsx_import_source.or_else(|| {
maybe_resolver.and_then(|r| {
r.default_jsx_import_source()
.map(|import_source| SpecifierWithRange {
text: import_source,
range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
})
})
});
if let Some(import_source) = res {
let jsx_import_source_module = maybe_resolver
.map(|r| r.jsx_import_source_module())
.unwrap_or(DEFAULT_JSX_IMPORT_SOURCE_MODULE);
let specifier =
format!("{}/{}", import_source.text, jsx_import_source_module);
let range =
Range::from_position_range(&module.specifier, &import_source.range);
let resolved_dependency = resolve(&specifier, &range, maybe_resolver);
let dep = module.dependencies.entry(specifier).or_default();
dep.maybe_code = resolved_dependency;
}
}
for specifier in module_info.jsdoc_imports {
let range = Range::from_position_range(&module.specifier, &specifier.range);
let resolved_dependency = resolve(&specifier.text, &range, maybe_resolver);
let dep = module.dependencies.entry(specifier.text).or_default();
dep.maybe_type = resolved_dependency;
}
if module.maybe_types_dependency.is_none() {
if let Some(headers) = maybe_headers {
if let Some(types_header) = headers.get("x-typescript-types") {
let range = Range {
specifier: module.specifier.clone(),
start: Position::zeroed(),
end: Position::zeroed(),
};
let resolved_dependency = resolve(types_header, &range, maybe_resolver);
module.maybe_types_dependency =
Some((types_header.clone(), resolved_dependency));
}
}
}
if let Some(resolver) = maybe_resolver {
if module.maybe_types_dependency.is_none() && is_untyped(&module.media_type)
{
module.maybe_types_dependency =
match resolver.resolve_types(&module.specifier) {
Ok(Some((specifier, maybe_range))) => Some((
module.specifier.to_string(),
Resolved::Ok {
specifier: specifier.clone(),
range: Box::new(maybe_range.unwrap_or_else(|| Range {
specifier,
start: Position::zeroed(),
end: Position::zeroed(),
})),
},
)),
Ok(None) => None,
Err(err) => Some((
module.specifier.to_string(),
Resolved::Err(ResolutionError::ResolverError {
error: Arc::new(err),
specifier: module.specifier.to_string(),
range: Range {
specifier: module.specifier.clone(),
start: Position::zeroed(),
end: Position::zeroed(),
},
}),
)),
};
}
}
for desc in module_info.dependencies {
let dep = module
.dependencies
.entry(desc.specifier.to_string())
.or_default();
dep.is_dynamic = desc.is_dynamic;
dep.maybe_assert_type = desc.import_assertions.get("type").cloned();
let resolved_dependency = resolve(
&desc.specifier,
&Range::from_position_range(&module.specifier, &desc.specifier_range),
maybe_resolver,
);
if matches!(
desc.kind,
DependencyKind::ImportType | DependencyKind::ExportType
) {
dep.maybe_type = resolved_dependency;
} else {
dep.maybe_code = resolved_dependency;
}
let specifier = module.specifier.clone();
let maybe_type = analyze_deno_types(&desc)
.map(|(text, range)| {
resolve(
&text,
&Range::from_position_range(&specifier, &range),
maybe_resolver,
)
})
.unwrap_or_else(|| Resolved::None);
if dep.maybe_type.is_none() {
dep.maybe_type = maybe_type
}
}
module
}
fn is_untyped(media_type: &MediaType) -> bool {
matches!(
media_type,
MediaType::JavaScript | MediaType::Jsx | MediaType::Mjs | MediaType::Cjs
)
}
pub(crate) enum BuildKind {
All,
CodeOnly,
TypesOnly,
}
pub type LoadWithSpecifierFuture =
Pin<Box<dyn Future<Output = (ModuleSpecifier, LoadResult)> + 'static>>;
pub(crate) struct Builder<'a> {
in_dynamic_branch: bool,
graph: ModuleGraph,
loader: &'a mut dyn Loader,
maybe_resolver: Option<&'a dyn Resolver>,
pending: FuturesUnordered<LoadWithSpecifierFuture>,
pending_assert_types: HashMap<ModuleSpecifier, HashSet<Option<String>>>,
dynamic_branches: HashMap<ModuleSpecifier, Option<String>>,
module_analyzer: &'a dyn ModuleAnalyzer,
maybe_reporter: Option<&'a dyn Reporter>,
}
impl<'a> Builder<'a> {
pub fn new(
roots: Vec<ModuleSpecifier>,
is_dynamic_root: bool,
loader: &'a mut dyn Loader,
maybe_resolver: Option<&'a dyn Resolver>,
module_analyzer: &'a dyn ModuleAnalyzer,
maybe_reporter: Option<&'a dyn Reporter>,
) -> Self {
Self {
in_dynamic_branch: is_dynamic_root,
graph: ModuleGraph::new(roots),
loader,
maybe_resolver,
pending: FuturesUnordered::new(),
pending_assert_types: HashMap::new(),
dynamic_branches: HashMap::new(),
module_analyzer,
maybe_reporter,
}
}
pub async fn build(
mut self,
build_kind: BuildKind,
maybe_imports: Option<Vec<(ModuleSpecifier, Vec<String>)>>,
) -> ModuleGraph {
let roots = self.graph.roots.clone();
for root in roots {
self.load(&root, self.in_dynamic_branch, None);
}
if let Some(imports) = maybe_imports {
for (referrer, imports) in imports {
let graph_import =
GraphImport::new(referrer, imports, self.maybe_resolver);
for dep in graph_import.dependencies.values() {
if let Resolved::Ok { specifier, .. } = &dep.maybe_type {
self.load(specifier, self.in_dynamic_branch, None);
}
}
self.graph.imports.push(graph_import);
}
}
loop {
let specifier = match self.pending.next().await {
Some((specifier, Ok(Some(response)))) => {
let assert_types =
self.pending_assert_types.remove(&specifier).unwrap();
for maybe_assert_type in assert_types {
self.visit(&specifier, &response, &build_kind, maybe_assert_type)
}
Some(specifier)
}
Some((specifier, Ok(None))) => {
self.graph.module_slots.insert(
specifier.clone(),
ModuleSlot::Err(ModuleGraphError::Missing(specifier.clone())),
);
Some(specifier)
}
Some((specifier, Err(err))) => {
self.graph.module_slots.insert(
specifier.clone(),
ModuleSlot::Err(ModuleGraphError::LoadingErr(
specifier.clone(),
Arc::new(err),
)),
);
Some(specifier)
}
_ => None,
};
if let (Some(specifier), Some(reporter)) =
(specifier, self.maybe_reporter)
{
let modules_total = self.graph.module_slots.len();
let modules_done = modules_total - self.pending.len();
reporter.on_load(&specifier, modules_done, modules_total);
}
if self.pending.is_empty() {
if !self.in_dynamic_branch {
self.in_dynamic_branch = true;
for (specifier, maybe_assert_type) in
std::mem::take(&mut self.dynamic_branches)
{
if !self.graph.module_slots.contains_key(&specifier) {
self.load(&specifier, true, maybe_assert_type.as_deref());
}
}
} else {
break;
}
}
}
for slot in self.graph.module_slots.values_mut() {
if let ModuleSlot::Module(ref mut module) = slot {
module.maybe_cache_info = self.loader.get_cache_info(&module.specifier);
}
}
self.graph
}
fn check_specifier(
&mut self,
requested_specifier: &ModuleSpecifier,
specifier: &ModuleSpecifier,
) {
if requested_specifier != specifier {
if let Some(slot) = self.graph.module_slots.get(requested_specifier) {
if matches!(slot, ModuleSlot::Pending) {
self.graph.module_slots.remove(requested_specifier);
}
}
self
.graph
.redirects
.insert(requested_specifier.clone(), specifier.clone());
}
}
fn load(
&mut self,
specifier: &ModuleSpecifier,
is_dynamic: bool,
maybe_assert_type: Option<&str>,
) {
let specifier = self.graph.redirects.get(specifier).unwrap_or(specifier);
let assert_types = self
.pending_assert_types
.entry(specifier.clone())
.or_default();
assert_types.insert(maybe_assert_type.map(String::from));
if !self.graph.module_slots.contains_key(specifier) {
self
.graph
.module_slots
.insert(specifier.clone(), ModuleSlot::Pending);
let specifier = specifier.clone();
let fut = self
.loader
.load(&specifier, is_dynamic)
.map(move |res| (specifier, res));
self.pending.push(Box::pin(fut));
}
}
fn roots_contain(&self, specifier: &ModuleSpecifier) -> bool {
for root in &self.graph.roots {
if root == specifier {
return true;
}
}
false
}
fn visit(
&mut self,
requested_specifier: &ModuleSpecifier,
response: &LoadResponse,
build_kind: &BuildKind,
maybe_assert_type: Option<String>,
) {
let (specifier, module_slot) = match response {
LoadResponse::External { specifier } => {
self.check_specifier(requested_specifier, specifier);
let module_slot = ModuleSlot::Module(Module::new_without_source(
specifier.clone(),
ModuleKind::External,
));
(specifier, module_slot)
}
LoadResponse::Module {
specifier,
content,
maybe_headers,
} => {
self.check_specifier(requested_specifier, specifier);
(
specifier,
self.visit_module(
specifier,
ModuleKind::Esm,
maybe_headers.as_ref(),
content.clone(),
build_kind,
maybe_assert_type,
),
)
}
};
self
.graph
.module_slots
.insert(specifier.clone(), module_slot);
}
fn visit_module(
&mut self,
specifier: &ModuleSpecifier,
kind: ModuleKind,
maybe_headers: Option<&HashMap<String, String>>,
content: Arc<str>,
build_kind: &BuildKind,
maybe_assert_type: Option<String>,
) -> ModuleSlot {
use std::borrow::BorrowMut;
let is_root = self.roots_contain(specifier);
let mut module_slot = parse_module(
specifier,
maybe_headers,
content,
maybe_assert_type.as_deref(),
Some(kind),
self.maybe_resolver,
self.module_analyzer,
is_root,
self.in_dynamic_branch,
);
if let ModuleSlot::Module(module) = module_slot.borrow_mut() {
if matches!(build_kind, BuildKind::All | BuildKind::CodeOnly)
|| module.maybe_types_dependency.is_none()
{
for dep in module.dependencies.values_mut() {
if matches!(build_kind, BuildKind::All | BuildKind::CodeOnly)
|| dep.maybe_type.is_none()
{
if let Resolved::Ok { specifier, .. } = &dep.maybe_code {
if dep.is_dynamic && !self.in_dynamic_branch {
self
.dynamic_branches
.insert(specifier.clone(), dep.maybe_assert_type.clone());
} else {
self.load(
specifier,
self.in_dynamic_branch,
dep.maybe_assert_type.as_deref(),
);
}
}
} else {
dep.maybe_code = Resolved::None;
}
if matches!(build_kind, BuildKind::All | BuildKind::TypesOnly) {
if let Resolved::Ok { specifier, .. } = &dep.maybe_type {
if dep.is_dynamic && !self.in_dynamic_branch {
self
.dynamic_branches
.insert(specifier.clone(), dep.maybe_assert_type.clone());
} else {
self.load(
specifier,
self.in_dynamic_branch,
dep.maybe_assert_type.as_deref(),
);
}
}
} else {
dep.maybe_type = Resolved::None;
}
}
} else {
module.dependencies.clear();
}
if matches!(build_kind, BuildKind::All | BuildKind::TypesOnly) {
if let Some((_, Resolved::Ok { specifier, .. })) =
&module.maybe_types_dependency
{
self.load(specifier, false, None);
}
} else {
module.maybe_types_dependency = None;
}
}
module_slot
}
}
struct SerializeableResolved<'a>(&'a Resolved);
impl<'a> Serialize for SerializeableResolved<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serialize_resolved(self.0, serializer)
}
}
struct SerializeableDependency<'a>(&'a str, &'a Dependency);
impl<'a> Serialize for SerializeableDependency<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("specifier", self.0)?;
if !self.1.maybe_code.is_none() {
let serializeable_resolved = SerializeableResolved(&self.1.maybe_code);
map.serialize_entry("code", &serializeable_resolved)?;
}
if !self.1.maybe_type.is_none() {
let serializeable_resolved = SerializeableResolved(&self.1.maybe_type);
map.serialize_entry("type", &serializeable_resolved)?;
}
if self.1.is_dynamic {
map.serialize_entry("isDynamic", &self.1.is_dynamic)?;
}
if self.1.maybe_assert_type.is_some() {
map.serialize_entry("assertionType", &self.1.maybe_assert_type)?;
}
map.end()
}
}
fn serialize_dependencies<S>(
dependencies: &BTreeMap<String, Dependency>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = serializer.serialize_seq(Some(dependencies.iter().count()))?;
for (specifier_str, dep) in dependencies.iter() {
let serializeable_dependency = SerializeableDependency(specifier_str, dep);
seq.serialize_element(&serializeable_dependency)?;
}
seq.end()
}
struct SerializeableModuleSlot<'a>(&'a ModuleSpecifier, &'a ModuleSlot);
impl<'a> Serialize for SerializeableModuleSlot<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self.1 {
ModuleSlot::Module(module) => Serialize::serialize(module, serializer),
ModuleSlot::Err(err) => {
let mut state = serializer.serialize_struct("ModuleSlot", 2)?;
state.serialize_field("specifier", self.0)?;
state.serialize_field("error", &err.to_string())?;
state.end()
}
ModuleSlot::Pending => {
let mut state = serializer.serialize_struct("ModuleSlot", 2)?;
state.serialize_field("specifier", self.0)?;
state.serialize_field(
"error",
"[INTERNAL ERROR] A pending module load never completed.",
)?;
state.end()
}
}
}
}
fn serialize_modules<S>(
modules: &BTreeMap<ModuleSpecifier, ModuleSlot>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = serializer.serialize_seq(Some(modules.iter().count()))?;
for (specifier, slot) in modules.iter() {
let serializeable_module_slot = SerializeableModuleSlot(specifier, slot);
seq.serialize_element(&serializeable_module_slot)?;
}
seq.end()
}
pub(crate) fn serialize_type_dependency<S>(
maybe_types_dependency: &Option<(String, Resolved)>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match *maybe_types_dependency {
Some((ref specifier, ref resolved)) => {
let mut state = serializer.serialize_struct("TypesDependency", 2)?;
state.serialize_field("specifier", specifier)?;
let serializeable_resolved = SerializeableResolved(resolved);
state.serialize_field("dependency", &serializeable_resolved)?;
state.end()
}
None => serializer.serialize_none(),
}
}
fn serialize_maybe_source<S>(
source: &Option<Arc<str>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(source) = source {
serializer.serialize_u32(source.len() as u32)
} else {
serializer.serialize_none()
}
}
#[cfg(test)]
mod tests {
use crate::DefaultModuleAnalyzer;
use super::*;
use url::Url;
#[test]
fn test_range_includes() {
let range = Range {
specifier: ModuleSpecifier::parse("file:///a.ts").unwrap(),
start: Position {
line: 1,
character: 20,
},
end: Position {
line: 1,
character: 30,
},
};
assert!(range.includes(&Position {
line: 1,
character: 20
}));
assert!(range.includes(&Position {
line: 1,
character: 25
}));
assert!(range.includes(&Position {
line: 1,
character: 30
}));
assert!(!range.includes(&Position {
line: 0,
character: 25
}));
assert!(!range.includes(&Position {
line: 2,
character: 25
}));
}
#[test]
fn test_module_dependency_includes() {
let specifier = ModuleSpecifier::parse("file:///a.ts").unwrap();
let module_analyzer = DefaultModuleAnalyzer::default();
let content = r#"import * as b from "./b.ts";"#;
let slot = parse_module(
&specifier,
None,
content.into(),
None,
Some(ModuleKind::Esm),
None,
&module_analyzer,
true,
false,
);
if let ModuleSlot::Module(module) = slot {
let maybe_dependency = module.dependencies.values().find_map(|d| {
d.includes(&Position {
line: 0,
character: 21,
})
.map(|r| (d, r))
});
assert!(maybe_dependency.is_some());
let (dependency, range) = maybe_dependency.unwrap();
assert_eq!(
dependency.maybe_code,
Resolved::Ok {
specifier: ModuleSpecifier::parse("file:///b.ts").unwrap(),
range: Box::new(Range {
specifier: specifier.clone(),
start: Position {
line: 0,
character: 19
},
end: Position {
line: 0,
character: 27
},
}),
}
);
assert_eq!(
range,
&Range {
specifier,
start: Position {
line: 0,
character: 19
},
end: Position {
line: 0,
character: 27
},
}
);
let maybe_dependency = module.dependencies.values().find_map(|d| {
d.includes(&Position {
line: 0,
character: 18,
})
});
assert!(maybe_dependency.is_none());
} else {
panic!("no module returned");
}
}
#[tokio::test]
async fn static_dep_of_dynamic_dep_is_dynamic() {
struct TestLoader {
loaded_foo: bool,
loaded_bar: bool,
loaded_baz: bool,
}
impl Loader for TestLoader {
fn load(
&mut self,
specifier: &ModuleSpecifier,
is_dynamic: bool,
) -> LoadFuture {
let specifier = specifier.clone();
match specifier.as_str() {
"file:///foo.js" => {
assert!(!is_dynamic);
self.loaded_foo = true;
Box::pin(async move {
Ok(Some(LoadResponse::Module {
specifier: specifier.clone(),
maybe_headers: None,
content: "await import('file:///bar.js')".into(),
}))
})
}
"file:///bar.js" => {
assert!(is_dynamic);
self.loaded_bar = true;
Box::pin(async move {
Ok(Some(LoadResponse::Module {
specifier: specifier.clone(),
maybe_headers: None,
content: "import 'file:///baz.js'".into(),
}))
})
}
"file:///baz.js" => {
assert!(is_dynamic);
self.loaded_baz = true;
Box::pin(async move {
Ok(Some(LoadResponse::Module {
specifier: specifier.clone(),
maybe_headers: None,
content: "console.log('Hello, world!')".into(),
}))
})
}
_ => unreachable!(),
}
}
}
let mut loader = TestLoader {
loaded_foo: false,
loaded_bar: false,
loaded_baz: false,
};
let module_analyzer = DefaultModuleAnalyzer::default();
let builder = Builder::new(
vec![Url::parse("file:///foo.js").unwrap()],
false,
&mut loader,
None,
&module_analyzer,
None,
);
builder.build(BuildKind::All, None).await;
assert!(loader.loaded_foo);
assert!(loader.loaded_bar);
assert!(loader.loaded_baz);
}
#[tokio::test]
async fn missing_module_is_error() {
struct TestLoader;
impl Loader for TestLoader {
fn load(
&mut self,
specifier: &ModuleSpecifier,
_is_dynamic: bool,
) -> LoadFuture {
let specifier = specifier.clone();
match specifier.as_str() {
"file:///foo.js" => Box::pin(async move {
Ok(Some(LoadResponse::Module {
specifier: specifier.clone(),
maybe_headers: None,
content: "await import('file:///bar.js')".into(),
}))
}),
"file:///bar.js" => Box::pin(async move { Ok(None) }),
_ => unreachable!(),
}
}
}
let mut loader = TestLoader;
let module_analyzer = DefaultModuleAnalyzer::default();
let builder = Builder::new(
vec![Url::parse("file:///foo.js").unwrap()],
false,
&mut loader,
None,
&module_analyzer,
None,
);
let graph = builder.build(BuildKind::All, None).await;
assert!(graph
.try_get(&Url::parse("file:///foo.js").unwrap())
.is_ok());
assert!(matches!(
graph
.try_get(&Url::parse("file:///bar.js").unwrap())
.unwrap_err(),
ModuleGraphError::Missing(..)
));
let specifiers = graph.specifiers().collect::<HashMap<_, _>>();
assert_eq!(specifiers.len(), 2);
assert!(specifiers
.get(&Url::parse("file:///foo.js").unwrap())
.unwrap()
.is_ok());
assert!(matches!(
specifiers
.get(&Url::parse("file:///bar.js").unwrap())
.unwrap()
.as_ref()
.unwrap_err(),
ModuleGraphError::Missing(..)
));
}
#[tokio::test]
async fn redirected_specifiers() {
struct TestLoader;
impl Loader for TestLoader {
fn load(
&mut self,
specifier: &ModuleSpecifier,
_is_dynamic: bool,
) -> LoadFuture {
let specifier = specifier.clone();
match specifier.as_str() {
"file:///foo.js" => Box::pin(async move {
Ok(Some(LoadResponse::Module {
specifier: Url::parse("file:///foo_actual.js").unwrap(),
maybe_headers: None,
content: "import 'file:///bar.js'".into(),
}))
}),
"file:///bar.js" => Box::pin(async move {
Ok(Some(LoadResponse::Module {
specifier: Url::parse("file:///bar_actual.js").unwrap(),
maybe_headers: None,
content: "(".into(),
}))
}),
_ => unreachable!(),
}
}
}
let mut loader = TestLoader;
let module_analyzer = DefaultModuleAnalyzer::default();
let builder = Builder::new(
vec![Url::parse("file:///foo.js").unwrap()],
false,
&mut loader,
None,
&module_analyzer,
None,
);
let graph = builder.build(BuildKind::All, None).await;
let specifiers = graph.specifiers().collect::<HashMap<_, _>>();
dbg!(&specifiers);
assert_eq!(specifiers.len(), 4);
assert!(specifiers
.get(&Url::parse("file:///foo.js").unwrap())
.unwrap()
.is_ok());
assert!(specifiers
.get(&Url::parse("file:///foo_actual.js").unwrap())
.unwrap()
.is_ok());
assert!(matches!(
specifiers
.get(&Url::parse("file:///bar.js").unwrap())
.unwrap()
.as_ref()
.unwrap_err(),
ModuleGraphError::ParseErr(..)
));
assert!(matches!(
specifiers
.get(&Url::parse("file:///bar_actual.js").unwrap())
.unwrap()
.as_ref()
.unwrap_err(),
ModuleGraphError::ParseErr(..)
));
}
#[test]
fn local_import_remote_module() {
let resolved = Resolved::from_specifier(
Url::parse("file:///local/mod.ts").unwrap(),
Range {
specifier: Url::parse("https://localhost").unwrap(),
start: Position::zeroed(),
end: Position::zeroed(),
},
false,
);
assert!(matches!(
resolved,
Resolved::Err(ResolutionError::InvalidLocalImport { .. })
));
}
#[test]
fn npm_import_remote_module() {
let resolved = Resolved::from_specifier(
Url::parse("npm:package").unwrap(),
Range {
specifier: Url::parse("https://localhost").unwrap(),
start: Position::zeroed(),
end: Position::zeroed(),
},
false,
);
assert!(matches!(resolved, Resolved::Ok { .. }));
}
}