use std::collections::HashMap;
use std::sync::Arc;
use deno_error::JsErrorBox;
use deno_media_type::MediaType;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::Deserialize;
use serde::Serialize;
use crate::ModuleSpecifier;
use crate::graph::Position;
use crate::graph::PositionRange;
use crate::source::ResolutionMode;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum DependencyDescriptor {
Static(StaticDependencyDescriptor),
Dynamic(DynamicDependencyDescriptor),
}
impl DependencyDescriptor {
pub fn as_static(&self) -> Option<&StaticDependencyDescriptor> {
match self {
Self::Static(descriptor) => Some(descriptor),
Self::Dynamic(_) => None,
}
}
pub fn as_dynamic(&self) -> Option<&DynamicDependencyDescriptor> {
match self {
Self::Static(_) => None,
Self::Dynamic(d) => Some(d),
}
}
pub fn import_attributes(&self) -> &ImportAttributes {
match self {
DependencyDescriptor::Static(d) => &d.import_attributes,
DependencyDescriptor::Dynamic(d) => &d.import_attributes,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum ImportAttribute {
Unknown,
Known(String),
}
#[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ImportAttributes {
#[default]
None,
Unknown,
Known(HashMap<String, ImportAttribute>),
}
impl ImportAttributes {
pub fn is_none(&self) -> bool {
matches!(self, ImportAttributes::None)
}
pub fn get(&self, key: &str) -> Option<&str> {
match self {
ImportAttributes::Known(map) => match map.get(key) {
Some(ImportAttribute::Known(value)) => Some(value),
_ => None,
},
_ => None,
}
}
pub fn has_asset(&self) -> bool {
let Some(value) = self.get("type") else {
return false;
};
matches!(value, "text" | "bytes")
}
}
#[derive(
Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize,
)]
#[serde(rename_all = "camelCase")]
pub enum DynamicDependencyKind {
#[default]
Import,
ImportDefer,
ImportSource,
Require,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum StaticDependencyKind {
Import,
ImportDefer,
ImportSource,
ImportType,
ImportEquals,
Export,
ExportType,
ExportEquals,
MaybeTsModuleAugmentation,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StaticDependencyDescriptor {
pub kind: StaticDependencyKind,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub types_specifier: Option<SpecifierWithRange>,
pub specifier: String,
pub specifier_range: PositionRange,
#[serde(skip_serializing_if = "is_false", default, rename = "sideEffect")]
pub is_side_effect: bool,
#[serde(skip_serializing_if = "ImportAttributes::is_none", default)]
pub import_attributes: ImportAttributes,
}
impl From<StaticDependencyDescriptor> for DependencyDescriptor {
fn from(descriptor: StaticDependencyDescriptor) -> Self {
DependencyDescriptor::Static(descriptor)
}
}
#[derive(Default, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", untagged)]
pub enum DynamicArgument {
String(String),
Template(Vec<DynamicTemplatePart>),
#[default]
Expr,
}
impl DynamicArgument {
pub fn is_expr(&self) -> bool {
matches!(self, DynamicArgument::Expr)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum DynamicTemplatePart {
String {
value: String,
},
Expr,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DynamicDependencyDescriptor {
#[serde(skip_serializing_if = "is_dynamic_esm", default)]
pub kind: DynamicDependencyKind,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub types_specifier: Option<SpecifierWithRange>,
#[serde(skip_serializing_if = "DynamicArgument::is_expr", default)]
pub argument: DynamicArgument,
pub argument_range: PositionRange,
#[serde(skip_serializing_if = "ImportAttributes::is_none", default)]
pub import_attributes: ImportAttributes,
}
fn is_dynamic_esm(kind: &DynamicDependencyKind) -> bool {
*kind == DynamicDependencyKind::Import
}
impl From<DynamicDependencyDescriptor> for DependencyDescriptor {
fn from(descriptor: DynamicDependencyDescriptor) -> Self {
DependencyDescriptor::Dynamic(descriptor)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpecifierWithRange {
pub text: String,
pub range: PositionRange,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TypeScriptTypesResolutionMode {
Require,
Import,
}
impl TypeScriptTypesResolutionMode {
#[allow(clippy::should_implement_trait)]
pub fn from_str(text: &str) -> Option<Self> {
match text {
"import" => Some(Self::Import),
"require" => Some(Self::Require),
_ => None,
}
}
pub fn as_deno_graph(&self) -> ResolutionMode {
match self {
Self::Require => ResolutionMode::Require,
Self::Import => ResolutionMode::Import,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum TypeScriptReference {
Path(SpecifierWithRange),
#[serde(rename_all = "camelCase")]
Types {
#[serde(flatten)]
specifier: SpecifierWithRange,
#[serde(skip_serializing_if = "Option::is_none", default)]
resolution_mode: Option<TypeScriptTypesResolutionMode>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JsDocImportInfo {
#[serde(flatten)]
pub specifier: SpecifierWithRange,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub resolution_mode: Option<TypeScriptTypesResolutionMode>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModuleInfo {
#[serde(skip_serializing_if = "is_false", default, rename = "script")]
pub is_script: bool,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub dependencies: Vec<DependencyDescriptor>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub ts_references: Vec<TypeScriptReference>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub self_types_specifier: Option<SpecifierWithRange>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub jsx_import_source: Option<SpecifierWithRange>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub jsx_import_source_types: Option<SpecifierWithRange>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub jsdoc_imports: Vec<JsDocImportInfo>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub source_map_url: Option<SpecifierWithRange>,
}
fn is_false(v: &bool) -> bool {
!v
}
pub fn module_graph_1_to_2(module_info: &mut serde_json::Value) {
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Comment {
text: String,
range: PositionRange,
}
fn analyze_deno_types(
leading_comments: &[Comment],
) -> Option<SpecifierWithRange> {
fn comment_position_to_position_range(
mut comment_start: Position,
range: std::ops::Range<usize>,
) -> PositionRange {
comment_start.character += 2;
PositionRange {
start: Position {
line: comment_start.line,
character: comment_start.character + range.start - 1,
},
end: Position {
line: comment_start.line,
character: comment_start.character + range.end + 1,
},
}
}
let comment = leading_comments.last()?;
let deno_types = find_deno_types(&comment.text)?;
Some(SpecifierWithRange {
text: deno_types.text.to_string(),
range: comment_position_to_position_range(
comment.range.start,
deno_types.range,
),
})
}
if let serde_json::Value::Object(module_info) = module_info
&& let Some(dependencies) = module_info
.get_mut("dependencies")
.and_then(|v| v.as_array_mut())
{
for dependency in dependencies {
if let Some(dependency) = dependency.as_object_mut()
&& let Some(leading_comments) = dependency
.get("leadingComments")
.and_then(|v| v.as_array())
.and_then(|v| {
v.iter()
.map(|v| serde_json::from_value(v.clone()).ok())
.collect::<Option<Vec<Comment>>>()
})
{
if let Some(deno_types) = analyze_deno_types(&leading_comments) {
dependency.insert(
"typesSpecifier".to_string(),
serde_json::to_value(deno_types).unwrap(),
);
}
dependency.remove("leadingComments");
}
}
};
}
#[async_trait::async_trait(?Send)]
pub trait ModuleAnalyzer {
async fn analyze(
&self,
specifier: &ModuleSpecifier,
source: Arc<str>,
media_type: MediaType,
) -> Result<ModuleInfo, JsErrorBox>;
}
impl<'a> Default for &'a dyn ModuleAnalyzer {
fn default() -> &'a dyn ModuleAnalyzer {
#[cfg(feature = "swc")]
{
&crate::ast::DefaultModuleAnalyzer
}
#[cfg(not(feature = "swc"))]
{
panic!(
"Provide a module analyzer or turn on the 'swc' cargo feature of deno_graph."
);
}
}
}
pub struct DenoTypesPragmaMatch<'a> {
pub text: &'a str,
pub range: std::ops::Range<usize>,
pub is_quoteless: bool,
}
pub fn is_comment_triple_slash_reference(comment_text: &str) -> bool {
static TRIPLE_SLASH_REFERENCE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?i)^/\s*<reference\s.*?/>").unwrap());
TRIPLE_SLASH_REFERENCE_RE.is_match(comment_text)
}
pub fn find_path_reference(text: &str) -> Option<regex::Match<'_>> {
static PATH_REFERENCE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"(?i)\spath\s*=\s*["']([^"']*)["']"#).unwrap());
PATH_REFERENCE_RE
.captures(text)
.and_then(|captures| captures.get(1))
}
pub fn find_types_reference(text: &str) -> Option<regex::Match<'_>> {
static TYPES_REFERENCE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"(?i)\stypes\s*=\s*["']([^"']*)["']"#).unwrap());
TYPES_REFERENCE_RE.captures(text).and_then(|c| c.get(1))
}
pub fn find_resolution_mode(text: &str) -> Option<regex::Match<'_>> {
static RESOLUTION_MODE_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"(?i)\sresolution-mode\s*=\s*["']([^"']*)["']"#).unwrap()
});
RESOLUTION_MODE_RE.captures(text).and_then(|m| m.get(1))
}
pub fn find_jsx_import_source(text: &str) -> Option<regex::Match<'_>> {
static JSX_IMPORT_SOURCE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?i)^[\s*]*@jsxImportSource\s+(\S+)").unwrap());
JSX_IMPORT_SOURCE_RE.captures(text).and_then(|c| c.get(1))
}
pub fn find_jsx_import_source_types(text: &str) -> Option<regex::Match<'_>> {
static JSX_IMPORT_SOURCE_TYPES_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)^[\s*]*@jsxImportSourceTypes\s+(\S+)").unwrap()
});
JSX_IMPORT_SOURCE_TYPES_RE
.captures(text)
.and_then(|c| c.get(1))
}
pub fn find_source_mapping_url(text: &str) -> Option<regex::Match<'_>> {
static SOURCE_MAPPING_URL_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)^[#@]\s*sourceMappingURL\s*=\s*(\S+)").unwrap()
});
SOURCE_MAPPING_URL_RE.captures(text).and_then(|c| c.get(1))
}
pub fn find_ts_self_types(text: &str) -> Option<regex::Match<'_>> {
static TS_SELF_TYPES_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"(?i)^\s*@ts-self-types\s*=\s*["']([^"']+)["']"#).unwrap()
});
TS_SELF_TYPES_RE.captures(text).and_then(|c| c.get(1))
}
pub fn find_ts_types(text: &str) -> Option<regex::Match<'_>> {
static TS_TYPES_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"(?i)^\s*@ts-types\s*=\s*["']([^"']+)["']"#).unwrap()
});
TS_TYPES_RE.captures(text).and_then(|c| c.get(1))
}
pub fn find_deno_types(text: &str) -> Option<DenoTypesPragmaMatch<'_>> {
static DENO_TYPES_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"(?i)^\s*@deno-types\s*=\s*(?:["']([^"']+)["']|(\S+))"#)
.unwrap()
});
let captures = DENO_TYPES_RE.captures(text)?;
if let Some(m) = captures.get(1) {
Some(DenoTypesPragmaMatch {
text: m.as_str(),
range: m.range(),
is_quoteless: false,
})
} else if let Some(m) = captures.get(2) {
Some(DenoTypesPragmaMatch {
text: m.as_str(),
range: m.range(),
is_quoteless: true,
})
} else {
unreachable!("Unexpected captures from deno types regex")
}
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
use pretty_assertions::assert_eq;
use serde::de::DeserializeOwned;
use serde_json::json;
use super::*;
#[test]
fn module_info_serialization_empty() {
let module_info = ModuleInfo {
is_script: false,
dependencies: Vec::new(),
ts_references: Vec::new(),
self_types_specifier: None,
jsx_import_source: None,
jsx_import_source_types: None,
jsdoc_imports: Vec::new(),
source_map_url: None,
};
run_serialization_test(&module_info, json!({}));
}
#[test]
fn module_info_serialization_deps() {
let module_info = ModuleInfo {
is_script: true,
dependencies: Vec::from([
StaticDependencyDescriptor {
kind: StaticDependencyKind::ImportEquals,
types_specifier: Some(SpecifierWithRange {
text: "a".to_string(),
range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
}),
specifier: "./test".to_string(),
specifier_range: PositionRange {
start: Position {
line: 1,
character: 2,
},
end: Position {
line: 3,
character: 4,
},
},
import_attributes: ImportAttributes::None,
is_side_effect: false,
}
.into(),
DynamicDependencyDescriptor {
kind: DynamicDependencyKind::Import,
types_specifier: None,
argument: DynamicArgument::String("./test2".to_string()),
argument_range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
import_attributes: ImportAttributes::Known(HashMap::from([
("key".to_string(), ImportAttribute::Unknown),
(
"key2".to_string(),
ImportAttribute::Known("value".to_string()),
),
("kind".to_string(), ImportAttribute::Unknown),
])),
}
.into(),
DynamicDependencyDescriptor {
kind: DynamicDependencyKind::Require,
types_specifier: None,
argument: DynamicArgument::String("./test3".to_string()),
argument_range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
import_attributes: ImportAttributes::None,
}
.into(),
]),
ts_references: Vec::new(),
self_types_specifier: None,
jsx_import_source: None,
jsx_import_source_types: None,
jsdoc_imports: Vec::new(),
source_map_url: None,
};
run_serialization_test(
&module_info,
json!({
"script": true,
"dependencies": [{
"type": "static",
"kind": "importEquals",
"typesSpecifier": {
"text": "a",
"range": [[0, 0], [0, 0]],
},
"specifier": "./test",
"specifierRange": [[1, 2], [3, 4]],
}, {
"type": "dynamic",
"argument": "./test2",
"argumentRange": [[0, 0], [0, 0]],
"importAttributes": {
"known": {
"key": null,
"kind": null,
"key2": "value",
}
}
}, {
"type": "dynamic",
"kind": "require",
"argument": "./test3",
"argumentRange": [[0, 0], [0, 0]]
}]
}),
);
}
#[test]
fn module_info_serialization_ts_references() {
let module_info = ModuleInfo {
is_script: false,
dependencies: Vec::new(),
ts_references: Vec::from([
TypeScriptReference::Path(SpecifierWithRange {
text: "a".to_string(),
range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
}),
TypeScriptReference::Types {
specifier: SpecifierWithRange {
text: "b".to_string(),
range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
},
resolution_mode: None,
},
TypeScriptReference::Types {
specifier: SpecifierWithRange {
text: "node".to_string(),
range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
},
resolution_mode: Some(TypeScriptTypesResolutionMode::Require),
},
TypeScriptReference::Types {
specifier: SpecifierWithRange {
text: "node-esm".to_string(),
range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
},
resolution_mode: Some(TypeScriptTypesResolutionMode::Import),
},
]),
self_types_specifier: None,
jsx_import_source: None,
jsx_import_source_types: None,
jsdoc_imports: Vec::new(),
source_map_url: None,
};
run_serialization_test(
&module_info,
json!({
"tsReferences": [{
"type": "path",
"text": "a",
"range": [[0, 0], [0, 0]],
}, {
"type": "types",
"text": "b",
"range": [[0, 0], [0, 0]],
}, {
"type": "types",
"text": "node",
"range": [[0, 0], [0, 0]],
"resolutionMode": "require",
}, {
"type": "types",
"text": "node-esm",
"range": [[0, 0], [0, 0]],
"resolutionMode": "import",
}]
}),
);
}
#[test]
fn module_info_serialization_self_types_specifier() {
let module_info = ModuleInfo {
is_script: false,
dependencies: Vec::new(),
ts_references: Vec::new(),
self_types_specifier: Some(SpecifierWithRange {
text: "a".to_string(),
range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
}),
jsx_import_source: None,
jsx_import_source_types: None,
jsdoc_imports: Vec::new(),
source_map_url: None,
};
run_serialization_test(
&module_info,
json!({
"selfTypesSpecifier": {
"text": "a",
"range": [[0, 0], [0, 0]],
}
}),
);
}
#[test]
fn module_info_serialization_jsx_import_source() {
let module_info = ModuleInfo {
is_script: false,
dependencies: Vec::new(),
ts_references: Vec::new(),
self_types_specifier: None,
jsx_import_source: Some(SpecifierWithRange {
text: "a".to_string(),
range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
}),
jsx_import_source_types: None,
jsdoc_imports: Vec::new(),
source_map_url: None,
};
run_serialization_test(
&module_info,
json!({
"jsxImportSource": {
"text": "a",
"range": [[0, 0], [0, 0]],
}
}),
);
}
#[test]
fn module_info_serialization_jsx_import_source_types() {
let module_info = ModuleInfo {
is_script: false,
dependencies: Vec::new(),
ts_references: Vec::new(),
self_types_specifier: None,
jsx_import_source: None,
jsx_import_source_types: Some(SpecifierWithRange {
text: "a".to_string(),
range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
}),
jsdoc_imports: Vec::new(),
source_map_url: None,
};
run_serialization_test(
&module_info,
json!({
"jsxImportSourceTypes": {
"text": "a",
"range": [[0, 0], [0, 0]],
}
}),
);
}
#[test]
fn module_info_jsdoc_imports() {
let module_info = ModuleInfo {
is_script: false,
dependencies: Vec::new(),
ts_references: Vec::new(),
self_types_specifier: None,
jsx_import_source: None,
jsx_import_source_types: None,
jsdoc_imports: Vec::from([
JsDocImportInfo {
specifier: SpecifierWithRange {
text: "a".to_string(),
range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
},
resolution_mode: None,
},
JsDocImportInfo {
specifier: SpecifierWithRange {
text: "b".to_string(),
range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
},
resolution_mode: Some(TypeScriptTypesResolutionMode::Import),
},
JsDocImportInfo {
specifier: SpecifierWithRange {
text: "c".to_string(),
range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
},
resolution_mode: Some(TypeScriptTypesResolutionMode::Require),
},
]),
source_map_url: None,
};
run_serialization_test(
&module_info,
json!({
"jsdocImports": [{
"text": "a",
"range": [[0, 0], [0, 0]],
}, {
"text": "b",
"range": [[0, 0], [0, 0]],
"resolutionMode": "import",
}, {
"text": "c",
"range": [[0, 0], [0, 0]],
"resolutionMode": "require",
}]
}),
);
}
#[test]
fn static_dependency_descriptor_serialization() {
let descriptor = DependencyDescriptor::Static(StaticDependencyDescriptor {
kind: StaticDependencyKind::ExportEquals,
types_specifier: Some(SpecifierWithRange {
text: "a".to_string(),
range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
}),
specifier: "./test".to_string(),
specifier_range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
import_attributes: ImportAttributes::Unknown,
is_side_effect: false,
});
run_serialization_test(
&descriptor,
json!({
"type": "static",
"kind": "exportEquals",
"typesSpecifier": {
"text": "a",
"range": [[0, 0], [0, 0]],
},
"specifier": "./test",
"specifierRange": [[0, 0], [0, 0]],
"importAttributes": "unknown",
}),
);
}
#[test]
fn static_dependency_descriptor_side_effect_serialization() {
let descriptor = DependencyDescriptor::Static(StaticDependencyDescriptor {
kind: StaticDependencyKind::ExportEquals,
types_specifier: None,
specifier: "./test".to_string(),
specifier_range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
import_attributes: ImportAttributes::Unknown,
is_side_effect: true,
});
run_serialization_test(
&descriptor,
json!({
"type": "static",
"kind": "exportEquals",
"specifier": "./test",
"specifierRange": [[0, 0], [0, 0]],
"importAttributes": "unknown",
"sideEffect": true,
}),
);
}
#[test]
fn static_dependency_descriptor_import_source_serialization() {
let descriptor = DependencyDescriptor::Static(StaticDependencyDescriptor {
kind: StaticDependencyKind::ImportSource,
types_specifier: None,
specifier: "./test".to_string(),
specifier_range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
import_attributes: ImportAttributes::None,
is_side_effect: false,
});
run_serialization_test(
&descriptor,
json!({
"type": "static",
"kind": "importSource",
"specifier": "./test",
"specifierRange": [[0, 0], [0, 0]],
}),
);
}
#[test]
fn dynamic_dependency_descriptor_serialization() {
run_serialization_test(
&DependencyDescriptor::Dynamic(DynamicDependencyDescriptor {
kind: DynamicDependencyKind::Import,
types_specifier: Some(SpecifierWithRange {
text: "a".to_string(),
range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
}),
argument: DynamicArgument::Expr,
argument_range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
import_attributes: ImportAttributes::Unknown,
}),
json!({
"type": "dynamic",
"typesSpecifier": {
"text": "a",
"range": [[0, 0], [0, 0]],
},
"argumentRange": [[0, 0], [0, 0]],
"importAttributes": "unknown",
}),
);
run_serialization_test(
&DependencyDescriptor::Dynamic(DynamicDependencyDescriptor {
kind: DynamicDependencyKind::Import,
types_specifier: None,
argument: DynamicArgument::String("test".to_string()),
argument_range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
import_attributes: ImportAttributes::Unknown,
}),
json!({
"type": "dynamic",
"argument": "test",
"argumentRange": [[0, 0], [0, 0]],
"importAttributes": "unknown",
}),
);
}
#[test]
fn dynamic_dependency_descriptor_import_source_serialization() {
let descriptor =
DependencyDescriptor::Dynamic(DynamicDependencyDescriptor {
kind: DynamicDependencyKind::ImportSource,
types_specifier: None,
argument: DynamicArgument::String("test".to_string()),
argument_range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
import_attributes: ImportAttributes::None,
});
run_serialization_test(
&descriptor,
json!({
"type": "dynamic",
"kind": "importSource",
"argument": "test",
"argumentRange": [[0, 0], [0, 0]],
}),
);
}
#[test]
fn test_dynamic_argument_serialization() {
run_serialization_test(
&DynamicArgument::String("test".to_string()),
json!("test"),
);
run_serialization_test(
&DynamicArgument::Template(vec![
DynamicTemplatePart::String {
value: "test".to_string(),
},
DynamicTemplatePart::Expr,
]),
json!([{
"type": "string",
"value": "test",
}, {
"type": "expr",
}]),
);
}
#[test]
fn test_import_attributes_serialization() {
run_serialization_test(&ImportAttributes::Unknown, json!("unknown"));
run_serialization_test(
&ImportAttributes::Known(HashMap::from([(
"type".to_string(),
ImportAttribute::Unknown,
)])),
json!({
"known": {
"type": null,
}
}),
);
run_serialization_test(
&ImportAttributes::Known(HashMap::from([(
"type".to_string(),
ImportAttribute::Known("test".to_string()),
)])),
json!({
"known": {
"type": "test",
}
}),
);
}
#[test]
fn test_v1_to_v2_deserialization_with_leading_comment() {
let expected = ModuleInfo {
is_script: false,
dependencies: vec![DependencyDescriptor::Static(
StaticDependencyDescriptor {
kind: StaticDependencyKind::Import,
specifier: "./a.js".to_string(),
specifier_range: PositionRange {
start: Position {
line: 1,
character: 2,
},
end: Position {
line: 3,
character: 4,
},
},
types_specifier: Some(SpecifierWithRange {
text: "./a.d.ts".to_string(),
range: PositionRange {
start: Position {
line: 0,
character: 15,
},
end: Position {
line: 0,
character: 25,
},
},
}),
import_attributes: ImportAttributes::None,
is_side_effect: false,
},
)],
ts_references: Vec::new(),
self_types_specifier: None,
jsx_import_source: None,
jsx_import_source_types: None,
jsdoc_imports: Vec::new(),
source_map_url: None,
};
let json = json!({
"dependencies": [{
"type": "static",
"kind": "import",
"specifier": "./a.js",
"specifierRange": [[1, 2], [3, 4]],
"leadingComments": [{
"text": " @deno-types=\"./a.d.ts\"",
"range": [[0, 0], [0, 25]],
}]
}]
});
run_v1_deserialization_test(json, &expected);
}
#[test]
fn test_v1_to_v2_deserialization_no_leading_comment() {
let expected = ModuleInfo {
is_script: false,
dependencies: vec![DependencyDescriptor::Static(
StaticDependencyDescriptor {
kind: StaticDependencyKind::Import,
specifier: "./a.js".to_string(),
specifier_range: PositionRange {
start: Position {
line: 1,
character: 2,
},
end: Position {
line: 3,
character: 4,
},
},
types_specifier: None,
import_attributes: ImportAttributes::None,
is_side_effect: false,
},
)],
ts_references: Vec::new(),
self_types_specifier: None,
jsx_import_source: None,
jsx_import_source_types: None,
jsdoc_imports: Vec::new(),
source_map_url: None,
};
let json = json!({
"dependencies": [{
"type": "static",
"kind": "import",
"specifier": "./a.js",
"specifierRange": [[1, 2], [3, 4]],
}]
});
run_v1_deserialization_test(json, &expected);
}
#[test]
fn test_v1_to_v2_deserialization_import_source_static() {
let expected = ModuleInfo {
is_script: false,
dependencies: vec![DependencyDescriptor::Static(
StaticDependencyDescriptor {
kind: StaticDependencyKind::ImportSource,
specifier: "./a.js".to_string(),
specifier_range: PositionRange {
start: Position {
line: 1,
character: 2,
},
end: Position {
line: 3,
character: 4,
},
},
types_specifier: None,
import_attributes: ImportAttributes::None,
is_side_effect: false,
},
)],
ts_references: Vec::new(),
self_types_specifier: None,
jsx_import_source: None,
jsx_import_source_types: None,
jsdoc_imports: Vec::new(),
source_map_url: None,
};
let json = json!({
"dependencies": [{
"type": "static",
"kind": "importSource",
"specifier": "./a.js",
"specifierRange": [[1, 2], [3, 4]],
}],
});
run_v1_deserialization_test(json, &expected);
}
#[test]
fn test_v1_to_v2_deserialization_import_source_dynamic() {
let expected = ModuleInfo {
is_script: false,
dependencies: vec![DependencyDescriptor::Dynamic(
DynamicDependencyDescriptor {
kind: DynamicDependencyKind::ImportSource,
types_specifier: None,
argument: DynamicArgument::String("./a.js".to_string()),
argument_range: PositionRange {
start: Position {
line: 1,
character: 2,
},
end: Position {
line: 3,
character: 4,
},
},
import_attributes: ImportAttributes::None,
},
)],
ts_references: Vec::new(),
self_types_specifier: None,
jsx_import_source: None,
jsx_import_source_types: None,
jsdoc_imports: Vec::new(),
source_map_url: None,
};
let json = json!({
"dependencies": [{
"type": "dynamic",
"kind": "importSource",
"argument": "./a.js",
"argumentRange": [[1, 2], [3, 4]],
}],
});
run_v1_deserialization_test(json, &expected);
}
#[test]
fn module_info_serialization_source_map_url() {
let module_info = ModuleInfo {
is_script: false,
dependencies: Vec::new(),
ts_references: Vec::new(),
self_types_specifier: None,
jsx_import_source: None,
jsx_import_source_types: None,
jsdoc_imports: Vec::new(),
source_map_url: Some(SpecifierWithRange {
text: "file.js.map".to_string(),
range: PositionRange {
start: Position::zeroed(),
end: Position::zeroed(),
},
}),
};
run_serialization_test(
&module_info,
json!({
"sourceMapUrl": {
"text": "file.js.map",
"range": [[0, 0], [0, 0]],
}
}),
);
}
#[track_caller]
fn run_serialization_test<
T: DeserializeOwned + Serialize + std::fmt::Debug + PartialEq + Eq,
>(
value: &T,
expected_json: serde_json::Value,
) {
let json = serde_json::to_value(value).unwrap();
assert_eq!(json, expected_json);
let deserialized_value = serde_json::from_value::<T>(json).unwrap();
assert_eq!(deserialized_value, *value);
}
#[track_caller]
fn run_v1_deserialization_test<
T: DeserializeOwned + Serialize + std::fmt::Debug + PartialEq + Eq,
>(
mut json: serde_json::Value,
value: &T,
) {
module_graph_1_to_2(&mut json);
let deserialized_value = serde_json::from_value::<T>(json).unwrap();
assert_eq!(deserialized_value, *value);
}
#[test]
fn test_find_source_mapping_url() {
let m = find_source_mapping_url("# sourceMappingURL=file.js.map");
assert!(m.is_some());
assert_eq!(m.unwrap().as_str(), "file.js.map");
let m = find_source_mapping_url("@ sourceMappingURL=file.js.map");
assert!(m.is_some());
assert_eq!(m.unwrap().as_str(), "file.js.map");
let m = find_source_mapping_url("# SOURCEMAPPINGURL=file.js.map");
assert!(m.is_some());
assert_eq!(m.unwrap().as_str(), "file.js.map");
let m = find_source_mapping_url("#sourceMappingURL=file.js.map");
assert!(m.is_some());
assert_eq!(m.unwrap().as_str(), "file.js.map");
let m = find_source_mapping_url("# sourceMappingURL = file.js.map");
assert!(m.is_some());
assert_eq!(m.unwrap().as_str(), "file.js.map");
let m = find_source_mapping_url("# sourceMappingURL=file.js.map?v=123");
assert!(m.is_some());
assert_eq!(m.unwrap().as_str(), "file.js.map?v=123");
let m = find_source_mapping_url(
"# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozfQ==",
);
assert!(m.is_some());
assert_eq!(
m.unwrap().as_str(),
"data:application/json;base64,eyJ2ZXJzaW9uIjozfQ=="
);
let m = find_source_mapping_url("# sourceMappingURL=../maps/file.js.map");
assert!(m.is_some());
assert_eq!(m.unwrap().as_str(), "../maps/file.js.map");
assert!(find_source_mapping_url("sourceMappingURL=file.js.map").is_none());
assert!(
find_source_mapping_url("// sourceMappingURL=file.js.map").is_none()
);
assert!(find_source_mapping_url("# sourceMappingURL").is_none());
}
}